|
| 1 | +"""Admin endpoints for managing application settings.""" |
| 2 | + |
| 3 | +import logging |
| 4 | +import hmac |
| 5 | + |
| 6 | +from fastapi import APIRouter, HTTPException, Request |
| 7 | +from pydantic import BaseModel, Field |
| 8 | +from typing import Optional |
| 9 | + |
| 10 | +from ..config import get_settings |
| 11 | +from ..notify import send_telegram_message |
| 12 | + |
| 13 | +log = logging.getLogger("uvicorn.error") |
| 14 | + |
| 15 | +router = APIRouter(tags=["admin"], prefix="/admin") |
| 16 | + |
| 17 | + |
| 18 | +class TelegramSettingsUpdate(BaseModel): |
| 19 | + """Request schema for updating Telegram settings.""" |
| 20 | + |
| 21 | + enabled: Optional[bool] = Field(None, description="Enable or disable Telegram notifications") |
| 22 | + bot_token: Optional[str] = Field(None, description="Telegram bot token (required if enabling)") |
| 23 | + chat_id: Optional[str] = Field(None, description="Telegram chat ID (required if enabling)") |
| 24 | + |
| 25 | + |
| 26 | +class TelegramSettingsResponse(BaseModel): |
| 27 | + """Response schema for Telegram settings.""" |
| 28 | + |
| 29 | + status: str = Field(description="Operation status") |
| 30 | + telegram_enabled: bool = Field(description="Whether Telegram notifications are enabled") |
| 31 | + telegram_bot_token: Optional[str] = Field(None, description="Current bot token (masked)") |
| 32 | + telegram_chat_id: Optional[str] = Field(None, description="Current chat ID") |
| 33 | + message: Optional[str] = Field(None, description="Operation message") |
| 34 | + |
| 35 | + |
| 36 | +def _validate_webhook_secret(request: Request) -> None: |
| 37 | + """Validate webhook secret from Authorization header. |
| 38 | +
|
| 39 | + Expects: Authorization: Bearer <secret> |
| 40 | + """ |
| 41 | + settings = request.app.state.settings |
| 42 | + env_secret = getattr(settings, "webhook_secret", None) |
| 43 | + |
| 44 | + if not env_secret: |
| 45 | + raise HTTPException(status_code=403, detail="Forbidden: webhook secret not configured") |
| 46 | + |
| 47 | + auth_header = request.headers.get("Authorization", "") |
| 48 | + if not auth_header.startswith("Bearer "): |
| 49 | + raise HTTPException(status_code=401, detail="Unauthorized: missing Bearer token") |
| 50 | + |
| 51 | + provided_secret = auth_header[7:] # Remove "Bearer " prefix |
| 52 | + expected_secret = env_secret.get_secret_value() |
| 53 | + |
| 54 | + if not hmac.compare_digest(provided_secret, expected_secret): |
| 55 | + raise HTTPException(status_code=401, detail="Unauthorized: invalid secret") |
| 56 | + |
| 57 | + |
| 58 | +def _mask_secret(secret: str, show_chars: int = 4) -> str: |
| 59 | + """Mask a secret string, showing only the last few characters.""" |
| 60 | + if not secret or len(secret) <= show_chars: |
| 61 | + return "***" |
| 62 | + return "*" * (len(secret) - show_chars) + secret[-show_chars:] |
| 63 | + |
| 64 | + |
| 65 | +@router.post( |
| 66 | + "/telegram", |
| 67 | + response_model=TelegramSettingsResponse, |
| 68 | + summary="Manage Telegram notification settings", |
| 69 | +) |
| 70 | +async def manage_telegram_settings( |
| 71 | + request: Request, |
| 72 | + settings_update: TelegramSettingsUpdate, |
| 73 | +) -> TelegramSettingsResponse: |
| 74 | + """Update Telegram notification settings. |
| 75 | +
|
| 76 | + Requires authentication via webhook secret in Authorization header: |
| 77 | + Authorization: Bearer <webhook_secret> |
| 78 | +
|
| 79 | + Args: |
| 80 | + enabled: Enable/disable Telegram notifications |
| 81 | + bot_token: Telegram bot token (required if enabling) |
| 82 | + chat_id: Telegram chat ID (required if enabling) |
| 83 | +
|
| 84 | + Returns: |
| 85 | + Updated Telegram settings status |
| 86 | + """ |
| 87 | + # Validate secret |
| 88 | + _validate_webhook_secret(request) |
| 89 | + |
| 90 | + app_settings = request.app.state.settings |
| 91 | + |
| 92 | + # Handle enable/disable |
| 93 | + if settings_update.enabled is not None: |
| 94 | + if settings_update.enabled and (not settings_update.bot_token or not settings_update.chat_id): |
| 95 | + raise HTTPException( |
| 96 | + status_code=400, |
| 97 | + detail="bot_token and chat_id are required when enabling Telegram" |
| 98 | + ) |
| 99 | + |
| 100 | + app_settings.telegram_enabled = settings_update.enabled |
| 101 | + |
| 102 | + if settings_update.enabled: |
| 103 | + app_settings.telegram_bot_token = settings_update.bot_token |
| 104 | + app_settings.telegram_chat_id = settings_update.chat_id |
| 105 | + |
| 106 | + # Update the telegram_notify function in app state |
| 107 | + def _telegram_notify(text: str, _token=settings_update.bot_token, _chat_id=settings_update.chat_id): |
| 108 | + return send_telegram_message(_token, _chat_id, text) |
| 109 | + |
| 110 | + request.app.state.telegram_notify = _telegram_notify |
| 111 | + log.info("Telegram notifications enabled via admin endpoint") |
| 112 | + else: |
| 113 | + request.app.state.telegram_notify = None |
| 114 | + log.info("Telegram notifications disabled via admin endpoint") |
| 115 | + |
| 116 | + # Handle token/chat_id updates when already enabled |
| 117 | + elif app_settings.telegram_enabled: |
| 118 | + if settings_update.bot_token: |
| 119 | + app_settings.telegram_bot_token = settings_update.bot_token |
| 120 | + |
| 121 | + if settings_update.chat_id: |
| 122 | + app_settings.telegram_chat_id = settings_update.chat_id |
| 123 | + |
| 124 | + # Re-create the notify function if either was updated |
| 125 | + if settings_update.bot_token or settings_update.chat_id: |
| 126 | + token = app_settings.telegram_bot_token |
| 127 | + chat_id = app_settings.telegram_chat_id |
| 128 | + |
| 129 | + def _telegram_notify(text: str, _token=token, _chat_id=chat_id): |
| 130 | + return send_telegram_message(_token, _chat_id, text) |
| 131 | + |
| 132 | + request.app.state.telegram_notify = _telegram_notify |
| 133 | + log.info("Telegram notification settings updated via admin endpoint") |
| 134 | + |
| 135 | + return TelegramSettingsResponse( |
| 136 | + status="ok", |
| 137 | + telegram_enabled=app_settings.telegram_enabled, |
| 138 | + telegram_bot_token=_mask_secret(app_settings.telegram_bot_token) if app_settings.telegram_bot_token else None, |
| 139 | + telegram_chat_id=app_settings.telegram_chat_id, |
| 140 | + message="Telegram settings updated successfully" |
| 141 | + ) |
0 commit comments