"""Authentik directory + messaging router. Resolves an ``authentik`` service instance from the registry, builds an :class:`AuthentikClient` from its config + decrypted ``api_token`` secret, and proxies paginated directory queries plus message-compose (email enqueue). Graceful "not configured" / "unreachable" payloads (matching the monitoring router's pattern) so the UI always renders. """ from __future__ import annotations import logging from typing import Any from fastapi import APIRouter, Depends from pydantic import BaseModel from media_library_viewer_api.clients.authentik import AuthentikClient from media_library_viewer_api.config import get_settings from media_library_viewer_api.dependencies import get_mail_queue, get_settings_store from media_library_viewer_api.services.mail_queue import MailQueue from media_library_viewer_api.services.mailer import validate_smtp_settings from media_library_viewer_api.services.service_resolution import resolve_service_record from media_library_viewer_api.services.settings_store import SettingsStore from media_library_viewer_api.widgets.sources import ServiceRecord logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/services/authentik", tags=["authentik"]) class MessageRequest(BaseModel): """Compose-request body for the Authentik messaging endpoint.""" recipient_emails: list[str] subject: str html_body: str def _build_client(service: ServiceRecord) -> AuthentikClient: base_url = str(service.config.get("base_url") or "").rstrip("/") api_token = str(service.secrets.get("api_token") or "") try: timeout = float(service.config.get("timeout_seconds") or 10) except (TypeError, ValueError): timeout = 10.0 return AuthentikClient(base_url=base_url, api_token=api_token, timeout=timeout) def _empty(error: str) -> dict[str, Any]: return {"items": [], "total": 0, "page": 1, "page_size": 50, "error": error} @router.get("/{service_id}/users") def get_authentik_users( service_id: str, search: str | None = None, page: int = 1, page_size: int = 50, store: SettingsStore = Depends(get_settings_store), ) -> dict[str, Any]: """Paginated Authentik user directory for a specific service instance.""" service = resolve_service_record(store, "authentik", service_id) if service is None: logger.info("Authentik users requested but no enabled authentik service for id=%s", service_id) return _empty("Authentik service not configured") try: client = _build_client(service) return client.users(search=search, page=page, page_size=page_size) except Exception: logger.exception("Authentik users query failed for service %s", service_id) return _empty("Authentik is unreachable") @router.get("/{service_id}/message/status") def get_authentik_message_status( service_id: str, store: SettingsStore = Depends(get_settings_store), mail_queue: MailQueue = Depends(get_mail_queue), ) -> dict[str, Any]: """Mail-queue status snapshot for the Authentik messaging tab.""" service = resolve_service_record(store, "authentik", service_id) if service is None: return {"state": "stopped", "worker_running": False, "error": "Authentik service not configured"} return mail_queue.status() @router.post("/{service_id}/message") def post_authentik_message( service_id: str, body: MessageRequest, store: SettingsStore = Depends(get_settings_store), mail_queue: MailQueue = Depends(get_mail_queue), ) -> dict[str, Any]: """Enqueue an email to Authentik-sourced recipients via the mail queue.""" service = resolve_service_record(store, "authentik", service_id) if service is None: return {"status": "error", "error": "Authentik service not configured"} recipients = [r.strip() for r in body.recipient_emails if r.strip()] if not recipients: return {"status": "error", "error": "No recipients with valid email addresses."} settings = get_settings() try: validate_smtp_settings(settings) except ValueError as exc: return {"status": "error", "error": f"SMTP settings invalid: {exc}"} request_id = mail_queue.enqueue( settings=settings, recipients=recipients, subject=body.subject, html_body=body.html_body, ) logger.info("Authentik message enqueued for service %s (%d recipients)", service_id, len(recipients)) return { "status": "queued", "request_id": request_id, "recipient_count": len(recipients), }