from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from sqlalchemy.orm import Session from app.auth.jwt_auth import get_current_user from app.database import get_db from app.services.notification_settings import ( VALID_SEVERITIES, TelegramConfig, WebhookConfig, get_effective_telegram_config, get_effective_webhook_config, upsert_telegram_channel, upsert_webhook_channel, ) from app.services.telegram_notify import TelegramNotConfiguredError, TelegramSendError, send_telegram_test_message from app.services.webhook_notify import WebhookNotConfiguredError, WebhookSendError, send_webhook_test_message router = APIRouter(prefix="/settings", tags=["settings"]) def _mask_secret(value: str, *, visible_tail: int = 4) -> str | None: text = value.strip() if not text: return None if len(text) <= visible_tail: return "*" * len(text) return f"{'*' * 8}…{text[-visible_tail:]}" def _mask_url(value: str) -> str | None: text = value.strip() if not text: return None if len(text) <= 20: return f"{'*' * 6}…{text[-4:]}" return f"{text[:16]}…{text[-6:]}" class TelegramSettingsResponse(BaseModel): enabled: bool configured: bool chat_id_hint: str | None = None bot_token_hint: str | None = None min_severity: str source: str = Field(description="env или db") class WebhookSettingsResponse(BaseModel): enabled: bool configured: bool url_hint: str | None = None secret_header: str | None = None secret_hint: str | None = None min_severity: str source: str = Field(description="env или db") class NotificationSettingsResponse(BaseModel): telegram: TelegramSettingsResponse webhook: WebhookSettingsResponse class TelegramSettingsUpdate(BaseModel): enabled: bool min_severity: str bot_token: str | None = None chat_id: str | None = None class WebhookSettingsUpdate(BaseModel): enabled: bool min_severity: str url: str | None = None secret_header: str | None = None secret: str | None = None class ChannelTestResponse(BaseModel): ok: bool = True message: str def _telegram_to_response(cfg: TelegramConfig) -> TelegramSettingsResponse: return TelegramSettingsResponse( enabled=cfg.enabled, configured=cfg.configured, chat_id_hint=_mask_secret(cfg.chat_id), bot_token_hint=_mask_secret(cfg.bot_token), min_severity=cfg.min_severity, source=cfg.source, ) def _webhook_to_response(cfg: WebhookConfig) -> WebhookSettingsResponse: return WebhookSettingsResponse( enabled=cfg.enabled, configured=cfg.configured, url_hint=_mask_url(cfg.url), secret_header=cfg.secret_header or None, secret_hint=_mask_secret(cfg.secret), min_severity=cfg.min_severity, source=cfg.source, ) @router.get("/notifications", response_model=NotificationSettingsResponse) def get_notification_settings( db: Session = Depends(get_db), _user: str = Depends(get_current_user), ) -> NotificationSettingsResponse: return NotificationSettingsResponse( telegram=_telegram_to_response(get_effective_telegram_config(db)), webhook=_webhook_to_response(get_effective_webhook_config(db)), ) @router.put("/notifications/telegram", response_model=TelegramSettingsResponse) def update_telegram_settings( body: TelegramSettingsUpdate, db: Session = Depends(get_db), _user: str = Depends(get_current_user), ) -> TelegramSettingsResponse: if body.min_severity not in VALID_SEVERITIES: raise HTTPException(status_code=422, detail=f"min_severity must be one of: {sorted(VALID_SEVERITIES)}") try: cfg = upsert_telegram_channel( db, enabled=body.enabled, min_severity=body.min_severity, bot_token=body.bot_token, chat_id=body.chat_id, ) except ValueError as exc: raise HTTPException(status_code=422, detail=str(exc)) from exc return _telegram_to_response(cfg) @router.put("/notifications/webhook", response_model=WebhookSettingsResponse) def update_webhook_settings( body: WebhookSettingsUpdate, db: Session = Depends(get_db), _user: str = Depends(get_current_user), ) -> WebhookSettingsResponse: if body.min_severity not in VALID_SEVERITIES: raise HTTPException(status_code=422, detail=f"min_severity must be one of: {sorted(VALID_SEVERITIES)}") try: cfg = upsert_webhook_channel( db, enabled=body.enabled, min_severity=body.min_severity, url=body.url, secret_header=body.secret_header, secret=body.secret, ) except ValueError as exc: raise HTTPException(status_code=422, detail=str(exc)) from exc return _webhook_to_response(cfg) @router.post("/notifications/telegram/test", response_model=ChannelTestResponse) def test_telegram_settings( db: Session = Depends(get_db), _user: str = Depends(get_current_user), ) -> ChannelTestResponse: cfg = get_effective_telegram_config(db) try: send_telegram_test_message(config=cfg) except TelegramNotConfiguredError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc except TelegramSendError as exc: code = exc.status_code if exc.status_code and 400 <= exc.status_code < 500 else 502 raise HTTPException(status_code=code, detail=str(exc)) from exc return ChannelTestResponse(message="Тестовое сообщение отправлено в Telegram") @router.post("/notifications/webhook/test", response_model=ChannelTestResponse) def test_webhook_settings( db: Session = Depends(get_db), _user: str = Depends(get_current_user), ) -> ChannelTestResponse: cfg = get_effective_webhook_config(db) try: send_webhook_test_message(config=cfg) except WebhookNotConfiguredError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc except WebhookSendError as exc: code = exc.status_code if exc.status_code and 400 <= exc.status_code < 500 else 502 raise HTTPException(status_code=code, detail=str(exc)) from exc return ChannelTestResponse(message="Тестовый POST отправлен на webhook URL")