"""Application settings: update interval + notification webhooks.""" from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException, Request from sqlmodel import Session, select from auth import get_current_user, require_admin from config import settings as env_settings from database import get_session from models.setting import ( ALL_EVENTS, WEBHOOK_TYPES, SettingsRead, SettingsUpdate, Webhook, WebhookCreate, WebhookRead, WebhookUpdate, ) from models.user import User from services import audit_service, notify_service, settings_service router = APIRouter(prefix="/api/settings", tags=["settings"]) def _ip(request: Request) -> str: return request.client.host if request.client else "unknown" def _to_read(wh: Webhook) -> WebhookRead: return WebhookRead( id=wh.id, name=wh.name, url=wh.url, type=wh.type, events=[e.strip() for e in (wh.events or "").split(",") if e.strip()], enabled=wh.enabled, created_at=wh.created_at, ) # --------------------------------------------------------------------------- # # General settings # --------------------------------------------------------------------------- # @router.get("", response_model=SettingsRead) def get_settings( session: Session = Depends(get_session), _user: User = Depends(get_current_user), ) -> SettingsRead: return SettingsRead( update_check_interval_minutes=settings_service.get_update_interval(session), env_webhook_count=len(env_settings.NOTIFY_WEBHOOKS), available_events=ALL_EVENTS, webhook_types=WEBHOOK_TYPES, ) @router.put("", response_model=SettingsRead) def update_settings( body: SettingsUpdate, request: Request, session: Session = Depends(get_session), user: User = Depends(require_admin), ) -> SettingsRead: if body.update_check_interval_minutes is not None: if body.update_check_interval_minutes < 5: raise HTTPException(status_code=400, detail="Interval must be at least 5 minutes") settings_service.set_value( session, settings_service.KEY_UPDATE_INTERVAL, body.update_check_interval_minutes, ) audit_service.record( session, user=user.username, action="settings.update", target="update_interval", detail=str(body.update_check_interval_minutes), ip=_ip(request), ) return get_settings(session, user) # --------------------------------------------------------------------------- # # Webhooks # --------------------------------------------------------------------------- # def _validate(wtype: str, events: list[str]) -> None: if wtype not in WEBHOOK_TYPES: raise HTTPException(status_code=400, detail=f"Unknown webhook type '{wtype}'") bad = [e for e in events if e not in ALL_EVENTS] if bad: raise HTTPException(status_code=400, detail=f"Unknown event(s): {', '.join(bad)}") @router.get("/webhooks", response_model=list[WebhookRead]) def list_webhooks( session: Session = Depends(get_session), _user: User = Depends(require_admin), ) -> list[WebhookRead]: rows = session.exec(select(Webhook).order_by(Webhook.id)).all() return [_to_read(w) for w in rows] @router.post("/webhooks", response_model=WebhookRead, status_code=201) def create_webhook( body: WebhookCreate, request: Request, session: Session = Depends(get_session), user: User = Depends(require_admin), ) -> WebhookRead: _validate(body.type, body.events) wh = Webhook( name=body.name, url=body.url, type=body.type, events=",".join(body.events), enabled=body.enabled, ) session.add(wh) session.commit() session.refresh(wh) audit_service.record( session, user=user.username, action="webhook.create", target=str(wh.id), detail=body.name, ip=_ip(request), ) return _to_read(wh) @router.put("/webhooks/{webhook_id}", response_model=WebhookRead) def update_webhook( webhook_id: int, body: WebhookUpdate, request: Request, session: Session = Depends(get_session), user: User = Depends(require_admin), ) -> WebhookRead: wh = session.get(Webhook, webhook_id) if not wh: raise HTTPException(status_code=404, detail="Webhook not found") if body.type is not None or body.events is not None: _validate(body.type or wh.type, body.events if body.events is not None else _to_read(wh).events) if body.name is not None: wh.name = body.name if body.url is not None: wh.url = body.url if body.type is not None: wh.type = body.type if body.events is not None: wh.events = ",".join(body.events) if body.enabled is not None: wh.enabled = body.enabled session.add(wh) session.commit() session.refresh(wh) audit_service.record( session, user=user.username, action="webhook.update", target=str(wh.id), ip=_ip(request), ) return _to_read(wh) @router.delete("/webhooks/{webhook_id}") def delete_webhook( webhook_id: int, request: Request, session: Session = Depends(get_session), user: User = Depends(require_admin), ) -> dict: wh = session.get(Webhook, webhook_id) if not wh: raise HTTPException(status_code=404, detail="Webhook not found") session.delete(wh) session.commit() audit_service.record( session, user=user.username, action="webhook.delete", target=str(webhook_id), ip=_ip(request), ) return {"ok": True} @router.post("/webhooks/{webhook_id}/test") async def test_webhook( webhook_id: int, session: Session = Depends(get_session), _user: User = Depends(require_admin), ) -> dict: wh = session.get(Webhook, webhook_id) if not wh: raise HTTPException(status_code=404, detail="Webhook not found") ok = await notify_service.test_webhook(wh.type, wh.url) return {"ok": ok}