Authentik Users + Messaging tabs + message endpoint (Slice 8)

Replace the UsersTab and MessagingTab stubs on the Authentik service
page, built new against the Authentik directory endpoint (the old
Jellyfin-backed Users page was deleted in slice 3).

Backend message endpoint (Option A -- implemented):
- POST /api/services/authentik/{service_id}/message accepts
  {recipient_emails, subject, html_body}, validates SMTP, enqueues via
  the existing mail_queue. Returns {status, request_id, recipient_count}
  on success or {status: 'error', error} on failure (200, matching the
  directory endpoint's graceful-error pattern).
- GET /api/services/authentik/{service_id}/message/status proxies
  mail_queue.status().

UsersTab: paginated (25/page), searchable directory table sourced from
GET /api/services/authentik/{id}/users. Columns: name, username, email,
status (is_active badge). Graceful error Alert on endpoint error.

MessagingTab: minimal but functional compose -- recipient search +
toggle buttons (Authentik users with emails), subject, HTML body
textarea (default template), send wired to the new endpoint, result
Alert. Rich-text toolbar, attachment upload, and queue-status banner
are follow-ups (the old compose UI had them; this slice ships the core
send flow).

New: api/authentik.ts, hooks/useAuthentik.ts (useAuthentikUsers +
useAuthentikMessageStatus), UsersTab + MessagingTab + tests. stubs.tsx
loses both stubs; index.ts wires the real components.

Tests: UsersTab (renders users + error state), MessagingTab (renders
compose form). 100 frontend tests pass (+4); 271 backend tests pass
(no regression); lint/build green both sides.

Refs openspec/changes/services-as-hub-ia/ (spec R6.2/R7.2/R7.3, tasks
slice 8).
This commit is contained in:
Developer
2026-06-26 19:44:35 +00:00
parent 6a1f8bbd59
commit 8f4e8428f0
9 changed files with 558 additions and 20 deletions
@@ -1,9 +1,10 @@
"""Authentik directory router — user lookup for the Authentik service page.
"""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 a paginated directory query. Graceful "not configured" / "unreachable"
payloads (matching the monitoring router's pattern) so the UI always renders.
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
@@ -12,9 +13,13 @@ 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.dependencies import get_settings_store
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.settings_store import SettingsStore
from media_library_viewer_api.widgets.sources import ServiceRecord, build_service_record
@@ -23,6 +28,14 @@ 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 _resolve_service_record(
store: SettingsStore,
service_id: str | None = None,
@@ -80,3 +93,52 @@ def get_authentik_users(
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, 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, 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),
}