refactor(ariadne): split app route registration

This commit is contained in:
codex 2026-04-21 01:50:12 -03:00
parent 18a6471c08
commit 0fa6138612
4 changed files with 741 additions and 833 deletions

View File

@ -1,71 +1,49 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
import json
import threading
from typing import Any, Callable
import sys
from typing import Any
from fastapi import Body, Depends, FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse, Response
from prometheus_client import CONTENT_TYPE_LATEST, generate_latest
from .app_account_routes import _register_account_routes
from .app_admin_routes import _register_admin_routes
from .auth.keycloak import AuthContext, authenticator
from .db.database import Database, DatabaseConfig
from .db.storage import Storage, TaskRunRecord
from .db.storage import Storage
from .manager.provisioning import ProvisioningManager
from .metrics.metrics import record_task_run
from .scheduler.cron import CronScheduler
from .services.cluster_state import run_cluster_state
from .services.comms import comms
from .services.firefly import firefly
from .services.image_sweeper import image_sweeper
from .services.jenkins_build_weather import collect_jenkins_build_weather
from .services.jenkins_workspace_cleanup import cleanup_jenkins_workspace_storage
from .services.keycloak_admin import keycloak_admin
from .services.keycloak_profile import run_profile_sync
from .services.mailu import mailu
from .services.mailu_events import mailu_events
from .services.nextcloud import nextcloud
from .services.image_sweeper import image_sweeper
from .services.jenkins_build_weather import collect_jenkins_build_weather
from .services.jenkins_workspace_cleanup import cleanup_jenkins_workspace_storage
from .services.metis import metis
from .services.metis_token_sync import metis_token_sync
from .services.nextcloud import nextcloud
from .services.opensearch_prune import prune_indices
from .services.platform_quality_probe import platform_quality_probe
from .services.pod_cleaner import clean_finished_pods
from .services.vaultwarden_sync import run_vaultwarden_sync
from .services.vault import vault
from .services.vaultwarden_sync import run_vaultwarden_sync
from .services.wger import wger
from .settings import settings
from .utils.errors import safe_error_detail
from .utils.http import extract_bearer_token
from .utils.logging import LogConfig, configure_logging, get_logger, task_context
from .utils.logging import LogConfig, configure_logging, get_logger
from .utils.passwords import random_password
configure_logging(LogConfig(level=settings.log_level))
logger = get_logger(__name__)
@dataclass(frozen=True)
class AccountTaskContext:
task_name: str
username: str
started: datetime
extra: dict[str, Any] | None = None
@dataclass(frozen=True)
class PasswordResetRequest:
task_name: str
service_label: str
username: str
mailu_email: str
password: str
sync_fn: Callable[[], dict[str, Any]]
password_attr: str
updated_attr: str
error_hint: str
portal_db = Database(
settings.portal_database_url,
DatabaseConfig(
@ -93,6 +71,7 @@ ariadne_db = Database(
storage = Storage(ariadne_db, portal_db)
provisioning = ProvisioningManager(portal_db, storage)
scheduler = CronScheduler(storage, settings.schedule_tick_sec)
app = FastAPI(title=settings.app_name)
def _record_event(event_type: str, detail: dict[str, Any] | str | None) -> None:
@ -111,9 +90,6 @@ def _parse_event_detail(detail: str | None) -> Any:
return detail
app = FastAPI(title=settings.app_name)
def _require_auth(request: Request) -> AuthContext:
token = extract_bearer_token(request)
if not token:
@ -169,92 +145,8 @@ def _allowed_flag_groups() -> list[str]:
return settings.allowed_flag_groups
def _resolve_mailu_email(username: str) -> str:
mailu_email = f"{username}@{settings.mailu_domain}"
try:
user = keycloak_admin.find_user(username) or {}
attrs = user.get("attributes") if isinstance(user, dict) else None
if isinstance(attrs, dict):
raw_mailu = attrs.get("mailu_email")
if isinstance(raw_mailu, list) and raw_mailu:
return str(raw_mailu[0])
if isinstance(raw_mailu, str) and raw_mailu:
return raw_mailu
except Exception:
return mailu_email
return mailu_email
def _record_account_task(ctx: AccountTaskContext, status: str, error_detail: str) -> None:
finished = datetime.now(timezone.utc)
duration_sec = (finished - ctx.started).total_seconds()
record_task_run(ctx.task_name, status, duration_sec)
try:
storage.record_task_run(
TaskRunRecord(
request_code=None,
task=ctx.task_name,
status=status,
detail=error_detail or None,
started_at=ctx.started,
finished_at=finished,
duration_ms=int(duration_sec * 1000),
)
)
except Exception:
pass
detail = {"username": ctx.username, "status": status, "error": error_detail}
if ctx.extra:
detail.update(ctx.extra)
_record_event(ctx.task_name, detail)
def _run_password_reset(request: PasswordResetRequest) -> JSONResponse:
started = datetime.now(timezone.utc)
task_ctx = AccountTaskContext(
task_name=request.task_name,
username=request.username,
started=started,
extra={"mailu_email": request.mailu_email},
)
status = "ok"
error_detail = ""
logger.info(
f"{request.service_label} password reset requested",
extra={"event": request.task_name, "username": request.username},
)
try:
result = request.sync_fn()
status_val = result.get("status") if isinstance(result, dict) else "error"
if status_val != "ok":
raise RuntimeError(f"{request.service_label} sync {status_val}")
keycloak_admin.set_user_attribute(
request.username,
request.password_attr,
request.password,
)
keycloak_admin.set_user_attribute(
request.username,
request.updated_attr,
datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
)
logger.info(
f"{request.service_label} password reset completed",
extra={"event": request.task_name, "username": request.username},
)
return JSONResponse({"status": "ok", "password": request.password})
except HTTPException as exc:
status = "error"
error_detail = str(exc.detail)
raise
except Exception as exc:
status = "error"
error_detail = safe_error_detail(exc, request.error_hint)
raise HTTPException(status_code=502, detail=error_detail)
finally:
_record_account_task(task_ctx, status, error_detail)
def _app_module() -> Any:
return sys.modules[__name__]
@app.on_event("startup")
@ -262,118 +154,34 @@ def _startup() -> None:
provisioning.start()
scheduler.add_task("schedule.mailu_sync", settings.mailu_sync_cron, lambda: mailu.sync("ariadne_schedule"))
scheduler.add_task(
"schedule.nextcloud_sync",
settings.nextcloud_sync_cron,
lambda: nextcloud.sync_mail(wait=False),
)
scheduler.add_task(
"schedule.nextcloud_cron",
settings.nextcloud_cron,
lambda: nextcloud.run_cron(),
)
scheduler.add_task(
"schedule.nextcloud_maintenance",
settings.nextcloud_maintenance_cron,
lambda: nextcloud.run_maintenance(),
)
scheduler.add_task("schedule.nextcloud_sync", settings.nextcloud_sync_cron, lambda: nextcloud.sync_mail(wait=False))
scheduler.add_task("schedule.nextcloud_cron", settings.nextcloud_cron, lambda: nextcloud.run_cron())
scheduler.add_task("schedule.nextcloud_maintenance", settings.nextcloud_maintenance_cron, lambda: nextcloud.run_maintenance())
scheduler.add_task("schedule.vaultwarden_sync", settings.vaultwarden_sync_cron, run_vaultwarden_sync)
scheduler.add_task(
"schedule.keycloak_profile",
settings.keycloak_profile_cron,
run_profile_sync,
)
scheduler.add_task(
"schedule.wger_user_sync",
settings.wger_user_sync_cron,
lambda: wger.sync_users(),
)
scheduler.add_task("schedule.keycloak_profile", settings.keycloak_profile_cron, run_profile_sync)
scheduler.add_task("schedule.wger_user_sync", settings.wger_user_sync_cron, lambda: wger.sync_users())
scheduler.add_task("schedule.wger_admin", settings.wger_admin_cron, lambda: wger.ensure_admin(wait=False))
scheduler.add_task(
"schedule.firefly_user_sync",
settings.firefly_user_sync_cron,
lambda: firefly.sync_users(),
)
scheduler.add_task(
"schedule.firefly_cron",
settings.firefly_cron,
lambda: firefly.run_cron(),
)
scheduler.add_task(
"schedule.pod_cleaner",
settings.pod_cleaner_cron,
clean_finished_pods,
)
scheduler.add_task(
"schedule.opensearch_prune",
settings.opensearch_prune_cron,
prune_indices,
)
scheduler.add_task(
"schedule.image_sweeper",
settings.image_sweeper_cron,
lambda: image_sweeper.run(wait=True),
)
scheduler.add_task(
"schedule.metis_sentinel_watch",
settings.metis_sentinel_watch_cron,
lambda: metis.watch_sentinel(),
)
scheduler.add_task(
"schedule.metis_k3s_token_sync",
settings.metis_k3s_token_sync_cron,
lambda: metis_token_sync.run(wait=True),
)
scheduler.add_task("schedule.firefly_user_sync", settings.firefly_user_sync_cron, lambda: firefly.sync_users())
scheduler.add_task("schedule.firefly_cron", settings.firefly_cron, lambda: firefly.run_cron())
scheduler.add_task("schedule.pod_cleaner", settings.pod_cleaner_cron, clean_finished_pods)
scheduler.add_task("schedule.opensearch_prune", settings.opensearch_prune_cron, prune_indices)
scheduler.add_task("schedule.image_sweeper", settings.image_sweeper_cron, lambda: image_sweeper.run(wait=True))
scheduler.add_task("schedule.metis_sentinel_watch", settings.metis_sentinel_watch_cron, lambda: metis.watch_sentinel())
scheduler.add_task("schedule.metis_k3s_token_sync", settings.metis_k3s_token_sync_cron, lambda: metis_token_sync.run(wait=True))
scheduler.add_task(
"schedule.platform_quality_suite_probe",
settings.platform_quality_suite_probe_cron,
lambda: platform_quality_probe.run(wait=True),
)
scheduler.add_task(
"schedule.jenkins_build_weather",
settings.jenkins_build_weather_cron,
collect_jenkins_build_weather,
)
scheduler.add_task(
"schedule.jenkins_workspace_cleanup",
settings.jenkins_workspace_cleanup_cron,
cleanup_jenkins_workspace_storage,
)
scheduler.add_task(
"schedule.vault_k8s_auth",
settings.vault_k8s_auth_cron,
lambda: vault.sync_k8s_auth(wait=True),
)
scheduler.add_task(
"schedule.vault_oidc",
settings.vault_oidc_cron,
lambda: vault.sync_oidc(wait=True),
)
scheduler.add_task(
"schedule.comms_guest_name",
settings.comms_guest_name_cron,
lambda: comms.run_guest_name_randomizer(wait=True),
)
scheduler.add_task(
"schedule.comms_pin_invite",
settings.comms_pin_invite_cron,
lambda: comms.run_pin_invite(wait=True),
)
scheduler.add_task(
"schedule.comms_reset_room",
settings.comms_reset_room_cron,
lambda: comms.run_reset_room(wait=True),
)
scheduler.add_task(
"schedule.comms_seed_room",
settings.comms_seed_room_cron,
lambda: comms.run_seed_room(wait=True),
)
scheduler.add_task(
"schedule.cluster_state",
settings.cluster_state_cron,
lambda: run_cluster_state(storage),
)
scheduler.add_task("schedule.jenkins_build_weather", settings.jenkins_build_weather_cron, collect_jenkins_build_weather)
scheduler.add_task("schedule.jenkins_workspace_cleanup", settings.jenkins_workspace_cleanup_cron, cleanup_jenkins_workspace_storage)
scheduler.add_task("schedule.vault_k8s_auth", settings.vault_k8s_auth_cron, lambda: vault.sync_k8s_auth(wait=True))
scheduler.add_task("schedule.vault_oidc", settings.vault_oidc_cron, lambda: vault.sync_oidc(wait=True))
scheduler.add_task("schedule.comms_guest_name", settings.comms_guest_name_cron, lambda: comms.run_guest_name_randomizer(wait=True))
scheduler.add_task("schedule.comms_pin_invite", settings.comms_pin_invite_cron, lambda: comms.run_pin_invite(wait=True))
scheduler.add_task("schedule.comms_reset_room", settings.comms_reset_room_cron, lambda: comms.run_reset_room(wait=True))
scheduler.add_task("schedule.comms_seed_room", settings.comms_seed_room_cron, lambda: comms.run_seed_room(wait=True))
scheduler.add_task("schedule.cluster_state", settings.cluster_state_cron, lambda: run_cluster_state(storage))
scheduler.start()
logger.info(
"ariadne started",
@ -435,614 +243,13 @@ def metrics() -> Response:
return Response(payload, media_type=CONTENT_TYPE_LATEST)
@app.get("/api/admin/access/requests")
def list_access_requests(ctx: AuthContext = Depends(_require_auth)) -> JSONResponse:
"""Return pending access requests for authenticated administrators."""
_require_admin(ctx)
logger.info(
"list access requests",
extra={"event": "access_requests_list", "actor": ctx.username or ""},
)
try:
rows = storage.list_pending_requests()
except Exception:
raise HTTPException(status_code=502, detail="failed to load requests")
output: list[dict[str, Any]] = []
for row in rows:
created_at = row.get("created_at")
output.append(
{
"id": row.get("request_code"),
"username": row.get("username"),
"email": row.get("contact_email") or "",
"first_name": row.get("first_name") or "",
"last_name": row.get("last_name") or "",
"request_code": row.get("request_code"),
"created_at": created_at.isoformat() if isinstance(created_at, datetime) else "",
"note": row.get("note") or "",
}
)
return JSONResponse({"requests": output})
@app.get("/api/admin/access/flags")
def list_access_flags(ctx: AuthContext = Depends(_require_auth)) -> JSONResponse:
"""Return Keycloak groups that can be applied as access-request flags."""
_require_admin(ctx)
flags = settings.allowed_flag_groups
if keycloak_admin.ready():
try:
flags = keycloak_admin.list_group_names(exclude={"admin"})
except Exception:
flags = settings.allowed_flag_groups
return JSONResponse({"flags": flags})
@app.get("/api/admin/audit/events")
def list_audit_events(
limit: int = 200,
event_type: str | None = None,
ctx: AuthContext = Depends(_require_auth),
) -> JSONResponse:
"""Return recent audit events with optional type filtering."""
_require_admin(ctx)
try:
rows = storage.list_events(limit=limit, event_type=event_type)
except Exception:
raise HTTPException(status_code=502, detail="failed to load audit events")
output: list[dict[str, Any]] = []
for row in rows:
created_at = row.get("created_at")
output.append(
{
"id": row.get("id"),
"event_type": row.get("event_type"),
"detail": _parse_event_detail(row.get("detail")),
"created_at": created_at.isoformat() if isinstance(created_at, datetime) else "",
}
)
return JSONResponse({"events": output})
@app.get("/api/admin/audit/task-runs")
def list_audit_task_runs(
limit: int = 200,
request_code: str | None = None,
task: str | None = None,
ctx: AuthContext = Depends(_require_auth),
) -> JSONResponse:
"""Return recorded background task runs for admin audit views."""
_require_admin(ctx)
try:
rows = storage.list_task_runs(limit=limit, request_code=request_code, task=task)
except Exception:
raise HTTPException(status_code=502, detail="failed to load task runs")
output: list[dict[str, Any]] = []
for row in rows:
started_at = row.get("started_at")
finished_at = row.get("finished_at")
output.append(
{
"id": row.get("id"),
"request_code": row.get("request_code") or "",
"task": row.get("task") or "",
"status": row.get("status") or "",
"detail": _parse_event_detail(row.get("detail")),
"started_at": started_at.isoformat() if isinstance(started_at, datetime) else "",
"finished_at": finished_at.isoformat() if isinstance(finished_at, datetime) else "",
"duration_ms": row.get("duration_ms"),
}
)
return JSONResponse({"task_runs": output})
@app.get("/api/admin/cluster/state")
def get_cluster_state(ctx: AuthContext = Depends(_require_auth)) -> JSONResponse:
"""Return the latest cluster-state snapshot to authenticated administrators."""
_require_admin(ctx)
snapshot = storage.latest_cluster_state()
if not snapshot:
raise HTTPException(status_code=404, detail="cluster state unavailable")
return JSONResponse(snapshot)
@app.get("/api/internal/cluster/state")
def get_cluster_state_internal() -> JSONResponse:
"""Return the latest cluster-state snapshot for trusted internal callers."""
snapshot = storage.latest_cluster_state()
if not snapshot:
raise HTTPException(status_code=404, detail="cluster state unavailable")
return JSONResponse(snapshot)
@app.post("/api/admin/access/requests/{username}/approve")
async def approve_access_request(
username: str,
request: Request,
ctx: AuthContext = Depends(_require_auth),
) -> JSONResponse:
"""Approve a verified access request and start account provisioning."""
_require_admin(ctx)
with task_context("admin.access.approve"):
payload = await _read_json_payload(request)
allowed_flags = _allowed_flag_groups()
flags = [flag for flag in _flags_from_payload(payload) if flag in allowed_flags]
note = _note_from_payload(payload)
decided_by = ctx.username or ""
try:
row = portal_db.fetchone(
"""
UPDATE access_requests
SET status = 'approved',
decided_at = NOW(),
decided_by = %s,
approval_flags = %s,
approval_note = %s
WHERE username = %s
AND status = 'pending'
AND email_verified_at IS NOT NULL
RETURNING request_code
""",
(decided_by or None, flags or None, note, username),
)
except Exception:
raise HTTPException(status_code=502, detail="failed to approve request")
if not row:
logger.info(
"access request approval ignored",
extra={"event": "access_request_approve", "actor": decided_by, "username": username, "status": "skipped"},
)
_record_event(
"access_request_approve",
{
"actor": decided_by,
"username": username,
"status": "skipped",
},
)
return JSONResponse({"ok": True, "request_code": ""})
request_code = row.get("request_code") or ""
if request_code:
threading.Thread(
target=provisioning.provision_access_request,
args=(request_code,),
daemon=True,
).start()
logger.info(
"access request approved",
extra={
"event": "access_request_approve",
"actor": decided_by,
"username": username,
"request_code": request_code,
},
)
_record_event(
"access_request_approve",
{
"actor": decided_by,
"username": username,
"request_code": request_code,
"status": "ok",
"flags": flags,
"note": note or "",
},
)
return JSONResponse({"ok": True, "request_code": request_code})
@app.post("/api/admin/access/requests/{username}/deny")
async def deny_access_request(
username: str,
request: Request,
ctx: AuthContext = Depends(_require_auth),
) -> JSONResponse:
"""Deny a pending access request and record the administrator decision."""
_require_admin(ctx)
with task_context("admin.access.deny"):
payload = await _read_json_payload(request)
note = _note_from_payload(payload)
decided_by = ctx.username or ""
try:
row = portal_db.fetchone(
"""
UPDATE access_requests
SET status = 'denied',
decided_at = NOW(),
decided_by = %s,
denial_note = %s
WHERE username = %s AND status = 'pending'
RETURNING request_code
""",
(decided_by or None, note, username),
)
except Exception:
raise HTTPException(status_code=502, detail="failed to deny request")
if not row:
logger.info(
"access request denial ignored",
extra={"event": "access_request_deny", "actor": decided_by, "username": username, "status": "skipped"},
)
_record_event(
"access_request_deny",
{
"actor": decided_by,
"username": username,
"status": "skipped",
},
)
return JSONResponse({"ok": True, "request_code": ""})
logger.info(
"access request denied",
extra={
"event": "access_request_deny",
"actor": decided_by,
"username": username,
"request_code": row.get("request_code") or "",
},
)
_record_event(
"access_request_deny",
{
"actor": decided_by,
"username": username,
"request_code": row.get("request_code") or "",
"status": "ok",
"note": note or "",
},
)
return JSONResponse({"ok": True, "request_code": row.get("request_code")})
@app.post("/api/access/requests/{request_code}/retry")
def retry_access_request(request_code: str) -> JSONResponse:
"""Reset failed provisioning tasks so an approved request can retry."""
code = (request_code or "").strip()
if not code:
raise HTTPException(status_code=400, detail="request_code is required")
if not keycloak_admin.ready():
raise HTTPException(status_code=503, detail="server not configured")
try:
row = portal_db.fetchone(
"SELECT status FROM access_requests WHERE request_code = %s",
(code,),
)
except Exception:
raise HTTPException(status_code=502, detail="failed to load request")
if not row:
raise HTTPException(status_code=404, detail="not found")
status = (row.get("status") or "").strip()
if status not in {"accounts_building", "approved"}:
raise HTTPException(status_code=409, detail="request not retryable")
try:
portal_db.execute(
"UPDATE access_requests SET provision_attempted_at = NULL WHERE request_code = %s",
(code,),
)
portal_db.execute(
"""
UPDATE access_request_tasks
SET status = 'pending',
detail = 'retry requested',
updated_at = NOW()
WHERE request_code = %s AND status = 'error'
""",
(code,),
)
except Exception:
raise HTTPException(status_code=502, detail="failed to update retry state")
threading.Thread(
target=provisioning.provision_access_request,
args=(code,),
daemon=True,
).start()
_record_event(
"access_request_retry",
{
"request_code": code,
"status": "ok",
},
)
return JSONResponse({"ok": True, "request_code": code})
@app.post("/api/account/mailu/rotate")
def rotate_mailu_password(ctx: AuthContext = Depends(_require_auth)) -> JSONResponse:
"""Rotate the caller's Mailu app password and trigger dependent syncs."""
_require_account_access(ctx)
if not keycloak_admin.ready():
raise HTTPException(status_code=503, detail="server not configured")
username = ctx.username or ""
if not username:
raise HTTPException(status_code=400, detail="missing username")
with task_context("account.mailu_rotate"):
started = datetime.now(timezone.utc)
status = "ok"
error_detail = ""
sync_enabled = mailu.ready()
sync_ok = False
sync_error = ""
nextcloud_sync: dict[str, Any] = {"status": "skipped"}
logger.info(
"mailu password rotate requested",
extra={"event": "mailu_rotate", "username": username},
)
try:
password = random_password()
keycloak_admin.set_user_attribute(username, "mailu_app_password", password)
if sync_enabled:
try:
mailu.sync("ariadne_mailu_rotate")
sync_ok = True
except Exception as exc:
sync_error = safe_error_detail(exc, "sync request failed")
try:
nextcloud_sync = nextcloud.sync_mail(username, wait=True)
except Exception as exc:
nextcloud_sync = {"status": "error", "detail": safe_error_detail(exc, "failed to sync nextcloud")}
logger.info(
"mailu password rotate completed",
extra={
"event": "mailu_rotate",
"username": username,
"sync_enabled": sync_enabled,
"sync_ok": sync_ok,
"nextcloud_status": nextcloud_sync.get("status") if isinstance(nextcloud_sync, dict) else "",
},
)
return JSONResponse(
{
"password": password,
"sync_enabled": sync_enabled,
"sync_ok": sync_ok,
"sync_error": sync_error,
"nextcloud_sync": nextcloud_sync,
}
)
except HTTPException as exc:
status = "error"
error_detail = str(exc.detail)
raise
except Exception as exc:
status = "error"
error_detail = safe_error_detail(exc, "mailu rotate failed")
raise HTTPException(status_code=502, detail=error_detail)
finally:
finished = datetime.now(timezone.utc)
duration_sec = (finished - started).total_seconds()
record_task_run("mailu_rotate", status, duration_sec)
try:
storage.record_task_run(
TaskRunRecord(
request_code=None,
task="mailu_rotate",
status=status,
detail=error_detail or None,
started_at=started,
finished_at=finished,
duration_ms=int(duration_sec * 1000),
)
)
except Exception:
pass
_record_event(
"mailu_rotate",
{
"username": username,
"status": status,
"sync_enabled": sync_enabled,
"sync_ok": sync_ok,
"nextcloud_status": nextcloud_sync.get("status") if isinstance(nextcloud_sync, dict) else "",
"error": error_detail,
},
)
@app.post("/api/account/wger/reset")
def reset_wger_password(ctx: AuthContext = Depends(_require_auth)) -> JSONResponse:
"""Reset the caller's Wger password and synchronize the service account."""
_require_account_access(ctx)
if not keycloak_admin.ready():
raise HTTPException(status_code=503, detail="server not configured")
username = ctx.username or ""
if not username:
raise HTTPException(status_code=400, detail="missing username")
with task_context("account.wger_reset"):
mailu_email = _resolve_mailu_email(username)
password = random_password()
request = PasswordResetRequest(
task_name="wger_reset",
service_label="wger",
username=username,
mailu_email=mailu_email,
password=password,
sync_fn=lambda: wger.sync_user(username, mailu_email, password, wait=True),
password_attr="wger_password",
updated_attr="wger_password_updated_at",
error_hint="wger sync failed",
)
return _run_password_reset(request)
@app.post("/api/account/firefly/reset")
def reset_firefly_password(ctx: AuthContext = Depends(_require_auth)) -> JSONResponse:
"""Reset the caller's Firefly password and synchronize the service account."""
_require_account_access(ctx)
if not keycloak_admin.ready():
raise HTTPException(status_code=503, detail="server not configured")
username = ctx.username or ""
if not username:
raise HTTPException(status_code=400, detail="missing username")
with task_context("account.firefly_reset"):
mailu_email = _resolve_mailu_email(username)
password = random_password(24)
request = PasswordResetRequest(
task_name="firefly_reset",
service_label="firefly",
username=username,
mailu_email=mailu_email,
password=password,
sync_fn=lambda: firefly.sync_user(mailu_email, password, wait=True),
password_attr="firefly_password",
updated_attr="firefly_password_updated_at",
error_hint="firefly sync failed",
)
return _run_password_reset(request)
@app.post("/api/account/firefly/rotation/check")
def firefly_rotation_check(ctx: AuthContext = Depends(_require_auth)) -> JSONResponse:
"""Check whether the caller's Firefly password rotation is healthy."""
_require_account_access(ctx)
if not keycloak_admin.ready():
raise HTTPException(status_code=503, detail="server not configured")
username = ctx.username or ""
if not username:
raise HTTPException(status_code=400, detail="missing username")
with task_context("account.firefly_rotation_check"):
result = firefly.check_rotation_for_user(username)
if result.get("status") == "error":
raise HTTPException(status_code=502, detail=result.get("detail") or "firefly rotation check failed")
return JSONResponse(result)
@app.post("/api/account/wger/rotation/check")
def wger_rotation_check(ctx: AuthContext = Depends(_require_auth)) -> JSONResponse:
"""Check whether the caller's Wger password rotation is healthy."""
_require_account_access(ctx)
if not keycloak_admin.ready():
raise HTTPException(status_code=503, detail="server not configured")
username = ctx.username or ""
if not username:
raise HTTPException(status_code=400, detail="missing username")
with task_context("account.wger_rotation_check"):
result = wger.check_rotation_for_user(username)
if result.get("status") == "error":
raise HTTPException(status_code=502, detail=result.get("detail") or "wger rotation check failed")
return JSONResponse(result)
@app.post("/api/account/nextcloud/mail/sync")
async def nextcloud_mail_sync(request: Request, ctx: AuthContext = Depends(_require_auth)) -> JSONResponse:
"""Synchronize the caller's Mailu address into Nextcloud mail settings."""
_require_account_access(ctx)
if not keycloak_admin.ready():
raise HTTPException(status_code=503, detail="server not configured")
username = ctx.username or ""
if not username:
raise HTTPException(status_code=400, detail="missing username")
with task_context("account.nextcloud_sync"):
try:
payload = await request.json()
except Exception:
payload = {}
wait = bool(payload.get("wait", True)) if isinstance(payload, dict) else True
started = datetime.now(timezone.utc)
status = "ok"
error_detail = ""
logger.info(
"nextcloud mail sync requested",
extra={"event": "nextcloud_sync", "username": username, "wait": wait},
)
try:
result = nextcloud.sync_mail(username, wait=wait)
logger.info(
"nextcloud mail sync completed",
extra={
"event": "nextcloud_sync",
"username": username,
"status": result.get("status") if isinstance(result, dict) else "",
},
)
return JSONResponse(result)
except HTTPException as exc:
status = "error"
error_detail = str(exc.detail)
raise
except Exception as exc:
status = "error"
error_detail = safe_error_detail(exc, "failed to sync nextcloud mail")
logger.info(
"nextcloud mail sync failed",
extra={"event": "nextcloud_sync", "username": username, "error": error_detail},
)
raise HTTPException(status_code=502, detail=error_detail)
finally:
finished = datetime.now(timezone.utc)
duration_sec = (finished - started).total_seconds()
record_task_run("nextcloud_sync", status, duration_sec)
try:
storage.record_task_run(
TaskRunRecord(
request_code=None,
task="nextcloud_sync",
status=status,
detail=error_detail or None,
started_at=started,
finished_at=finished,
duration_ms=int(duration_sec * 1000),
)
)
except Exception:
pass
_record_event(
"nextcloud_sync",
{
"username": username,
"status": status,
"wait": wait,
"error": error_detail,
},
)
@app.post("/events")
def mailu_event_listener(payload: dict[str, Any] | None = Body(default=None)) -> Response:
"""Accept Mailu webhook events and dispatch mapped account actions."""
status_code, response = mailu_events.handle_event(payload)
return JSONResponse(response, status_code=status_code)
_register_admin_routes(app, _require_auth, _app_module)
_register_account_routes(app, _require_auth, _app_module)

View File

@ -0,0 +1,356 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Callable
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
from .auth.keycloak import AuthContext
from .db.storage import TaskRunRecord
from .utils.errors import safe_error_detail
from .utils.logging import task_context
@dataclass(frozen=True)
class AccountTaskContext:
task_name: str
username: str
started: datetime
extra: dict[str, Any] | None = None
@dataclass(frozen=True)
class PasswordResetRequest:
task_name: str
service_label: str
username: str
mailu_email: str
password: str
sync_fn: Callable[[], dict[str, Any]]
password_attr: str
updated_attr: str
error_hint: str
def _resolve_mailu_email(module: Any, username: str) -> str:
mailu_email = f"{username}@{module.settings.mailu_domain}"
try:
user = module.keycloak_admin.find_user(username) or {}
attrs = user.get("attributes") if isinstance(user, dict) else None
if isinstance(attrs, dict):
raw_mailu = attrs.get("mailu_email")
if isinstance(raw_mailu, list) and raw_mailu:
return str(raw_mailu[0])
if isinstance(raw_mailu, str) and raw_mailu:
return raw_mailu
except Exception:
return mailu_email
return mailu_email
def _record_account_task(module: Any, ctx: AccountTaskContext, status: str, error_detail: str) -> None:
finished = datetime.now(timezone.utc)
duration_sec = (finished - ctx.started).total_seconds()
module.record_task_run(ctx.task_name, status, duration_sec)
try:
module.storage.record_task_run(
TaskRunRecord(
request_code=None,
task=ctx.task_name,
status=status,
detail=error_detail or None,
started_at=ctx.started,
finished_at=finished,
duration_ms=int(duration_sec * 1000),
)
)
except Exception:
pass
detail = {"username": ctx.username, "status": status, "error": error_detail}
if ctx.extra:
detail.update(ctx.extra)
module._record_event(ctx.task_name, detail)
def _run_password_reset(module: Any, request: PasswordResetRequest) -> JSONResponse:
started = datetime.now(timezone.utc)
task_ctx = AccountTaskContext(
task_name=request.task_name,
username=request.username,
started=started,
extra={"mailu_email": request.mailu_email},
)
status = "ok"
error_detail = ""
module.logger.info(
f"{request.service_label} password reset requested",
extra={"event": request.task_name, "username": request.username},
)
try:
result = request.sync_fn()
status_val = result.get("status") if isinstance(result, dict) else "error"
if status_val != "ok":
raise RuntimeError(f"{request.service_label} sync {status_val}")
module.keycloak_admin.set_user_attribute(request.username, request.password_attr, request.password)
module.keycloak_admin.set_user_attribute(
request.username,
request.updated_attr,
datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
)
module.logger.info(
f"{request.service_label} password reset completed",
extra={"event": request.task_name, "username": request.username},
)
return JSONResponse({"status": "ok", "password": request.password})
except HTTPException as exc:
status = "error"
error_detail = str(exc.detail)
raise
except Exception as exc:
status = "error"
error_detail = safe_error_detail(exc, request.error_hint)
raise HTTPException(status_code=502, detail=error_detail)
finally:
_record_account_task(module, task_ctx, status, error_detail)
def _register_account_routes(app: FastAPI, require_auth: Callable, deps: Callable[[], Any]) -> None: # noqa: PLR0915
@app.post("/api/account/mailu/rotate")
def rotate_mailu_password(ctx: AuthContext = Depends(require_auth)) -> JSONResponse:
"""Rotate the caller's Mailu app password and trigger dependent syncs."""
module = deps()
module._require_account_access(ctx)
if not module.keycloak_admin.ready():
raise HTTPException(status_code=503, detail="server not configured")
username = ctx.username or ""
if not username:
raise HTTPException(status_code=400, detail="missing username")
with task_context("account.mailu_rotate"):
started = datetime.now(timezone.utc)
status = "ok"
error_detail = ""
sync_enabled = module.mailu.ready()
sync_ok = False
sync_error = ""
nextcloud_sync: dict[str, Any] = {"status": "skipped"}
module.logger.info("mailu password rotate requested", extra={"event": "mailu_rotate", "username": username})
try:
password = module.random_password()
module.keycloak_admin.set_user_attribute(username, "mailu_app_password", password)
if sync_enabled:
try:
module.mailu.sync("ariadne_mailu_rotate")
sync_ok = True
except Exception as exc:
sync_error = safe_error_detail(exc, "sync request failed")
try:
nextcloud_sync = module.nextcloud.sync_mail(username, wait=True)
except Exception as exc:
nextcloud_sync = {"status": "error", "detail": safe_error_detail(exc, "failed to sync nextcloud")}
module.logger.info(
"mailu password rotate completed",
extra={
"event": "mailu_rotate",
"username": username,
"sync_enabled": sync_enabled,
"sync_ok": sync_ok,
"nextcloud_status": nextcloud_sync.get("status") if isinstance(nextcloud_sync, dict) else "",
},
)
return JSONResponse(
{
"password": password,
"sync_enabled": sync_enabled,
"sync_ok": sync_ok,
"sync_error": sync_error,
"nextcloud_sync": nextcloud_sync,
}
)
except HTTPException as exc:
status = "error"
error_detail = str(exc.detail)
raise
except Exception as exc:
status = "error"
error_detail = safe_error_detail(exc, "mailu rotate failed")
raise HTTPException(status_code=502, detail=error_detail)
finally:
task_ctx = AccountTaskContext("mailu_rotate", username, started)
_record_account_task(module, task_ctx, status, error_detail)
module._record_event(
"mailu_rotate",
{
"username": username,
"status": status,
"sync_enabled": sync_enabled,
"sync_ok": sync_ok,
"nextcloud_status": nextcloud_sync.get("status") if isinstance(nextcloud_sync, dict) else "",
"error": error_detail,
},
)
@app.post("/api/account/wger/reset")
def reset_wger_password(ctx: AuthContext = Depends(require_auth)) -> JSONResponse:
"""Reset the caller's Wger password and synchronize the service account."""
module = deps()
module._require_account_access(ctx)
if not module.keycloak_admin.ready():
raise HTTPException(status_code=503, detail="server not configured")
username = ctx.username or ""
if not username:
raise HTTPException(status_code=400, detail="missing username")
with task_context("account.wger_reset"):
mailu_email = _resolve_mailu_email(module, username)
password = module.random_password()
request = PasswordResetRequest(
task_name="wger_reset",
service_label="wger",
username=username,
mailu_email=mailu_email,
password=password,
sync_fn=lambda: module.wger.sync_user(username, mailu_email, password, wait=True),
password_attr="wger_password",
updated_attr="wger_password_updated_at",
error_hint="wger sync failed",
)
return _run_password_reset(module, request)
@app.post("/api/account/firefly/reset")
def reset_firefly_password(ctx: AuthContext = Depends(require_auth)) -> JSONResponse:
"""Reset the caller's Firefly password and synchronize the service account."""
module = deps()
module._require_account_access(ctx)
if not module.keycloak_admin.ready():
raise HTTPException(status_code=503, detail="server not configured")
username = ctx.username or ""
if not username:
raise HTTPException(status_code=400, detail="missing username")
with task_context("account.firefly_reset"):
mailu_email = _resolve_mailu_email(module, username)
password = module.random_password(24)
request = PasswordResetRequest(
task_name="firefly_reset",
service_label="firefly",
username=username,
mailu_email=mailu_email,
password=password,
sync_fn=lambda: module.firefly.sync_user(mailu_email, password, wait=True),
password_attr="firefly_password",
updated_attr="firefly_password_updated_at",
error_hint="firefly sync failed",
)
return _run_password_reset(module, request)
@app.post("/api/account/firefly/rotation/check")
def firefly_rotation_check(ctx: AuthContext = Depends(require_auth)) -> JSONResponse:
"""Check whether the caller's Firefly password rotation is healthy."""
module = deps()
module._require_account_access(ctx)
if not module.keycloak_admin.ready():
raise HTTPException(status_code=503, detail="server not configured")
username = ctx.username or ""
if not username:
raise HTTPException(status_code=400, detail="missing username")
with task_context("account.firefly_rotation_check"):
result = module.firefly.check_rotation_for_user(username)
if result.get("status") == "error":
raise HTTPException(status_code=502, detail=result.get("detail") or "firefly rotation check failed")
return JSONResponse(result)
@app.post("/api/account/wger/rotation/check")
def wger_rotation_check(ctx: AuthContext = Depends(require_auth)) -> JSONResponse:
"""Check whether the caller's Wger password rotation is healthy."""
module = deps()
module._require_account_access(ctx)
if not module.keycloak_admin.ready():
raise HTTPException(status_code=503, detail="server not configured")
username = ctx.username or ""
if not username:
raise HTTPException(status_code=400, detail="missing username")
with task_context("account.wger_rotation_check"):
result = module.wger.check_rotation_for_user(username)
if result.get("status") == "error":
raise HTTPException(status_code=502, detail=result.get("detail") or "wger rotation check failed")
return JSONResponse(result)
@app.post("/api/account/nextcloud/mail/sync")
async def nextcloud_mail_sync(request: Request, ctx: AuthContext = Depends(require_auth)) -> JSONResponse:
"""Synchronize the caller's Mailu address into Nextcloud mail settings."""
module = deps()
module._require_account_access(ctx)
if not module.keycloak_admin.ready():
raise HTTPException(status_code=503, detail="server not configured")
username = ctx.username or ""
if not username:
raise HTTPException(status_code=400, detail="missing username")
with task_context("account.nextcloud_sync"):
try:
payload = await request.json()
except Exception:
payload = {}
wait = bool(payload.get("wait", True)) if isinstance(payload, dict) else True
started = datetime.now(timezone.utc)
status = "ok"
error_detail = ""
module.logger.info("nextcloud mail sync requested", extra={"event": "nextcloud_sync", "username": username, "wait": wait})
try:
result = module.nextcloud.sync_mail(username, wait=wait)
module.logger.info(
"nextcloud mail sync completed",
extra={
"event": "nextcloud_sync",
"username": username,
"status": result.get("status") if isinstance(result, dict) else "",
},
)
return JSONResponse(result)
except HTTPException as exc:
status = "error"
error_detail = str(exc.detail)
raise
except Exception as exc:
status = "error"
error_detail = safe_error_detail(exc, "failed to sync nextcloud mail")
module.logger.info(
"nextcloud mail sync failed",
extra={"event": "nextcloud_sync", "username": username, "error": error_detail},
)
raise HTTPException(status_code=502, detail=error_detail)
finally:
task_ctx = AccountTaskContext("nextcloud_sync", username, started)
_record_account_task(module, task_ctx, status, error_detail)
module._record_event(
"nextcloud_sync",
{
"username": username,
"status": status,
"wait": wait,
"error": error_detail,
},
)

346
ariadne/app_admin_routes.py Normal file
View File

@ -0,0 +1,346 @@
from __future__ import annotations
from datetime import datetime
import threading
from typing import Any, Callable
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
from .auth.keycloak import AuthContext
from .utils.logging import task_context
def _register_admin_routes(app: FastAPI, require_auth: Callable, deps: Callable[[], Any]) -> None: # noqa: PLR0915
@app.get("/api/admin/access/requests")
def list_access_requests(ctx: AuthContext = Depends(require_auth)) -> JSONResponse:
"""Return pending access requests for authenticated administrators."""
module = deps()
module._require_admin(ctx)
module.logger.info(
"list access requests",
extra={"event": "access_requests_list", "actor": ctx.username or ""},
)
try:
rows = module.storage.list_pending_requests()
except Exception:
raise HTTPException(status_code=502, detail="failed to load requests")
output: list[dict[str, Any]] = []
for row in rows:
created_at = row.get("created_at")
output.append(
{
"id": row.get("request_code"),
"username": row.get("username"),
"email": row.get("contact_email") or "",
"first_name": row.get("first_name") or "",
"last_name": row.get("last_name") or "",
"request_code": row.get("request_code"),
"created_at": created_at.isoformat() if isinstance(created_at, datetime) else "",
"note": row.get("note") or "",
}
)
return JSONResponse({"requests": output})
@app.get("/api/admin/access/flags")
def list_access_flags(ctx: AuthContext = Depends(require_auth)) -> JSONResponse:
"""Return Keycloak groups that can be applied as access-request flags."""
module = deps()
module._require_admin(ctx)
flags = module.settings.allowed_flag_groups
if module.keycloak_admin.ready():
try:
flags = module.keycloak_admin.list_group_names(exclude={"admin"})
except Exception:
flags = module.settings.allowed_flag_groups
return JSONResponse({"flags": flags})
@app.get("/api/admin/audit/events")
def list_audit_events(
limit: int = 200,
event_type: str | None = None,
ctx: AuthContext = Depends(require_auth),
) -> JSONResponse:
"""Return recent audit events with optional type filtering."""
module = deps()
module._require_admin(ctx)
try:
rows = module.storage.list_events(limit=limit, event_type=event_type)
except Exception:
raise HTTPException(status_code=502, detail="failed to load audit events")
output: list[dict[str, Any]] = []
for row in rows:
created_at = row.get("created_at")
output.append(
{
"id": row.get("id"),
"event_type": row.get("event_type"),
"detail": module._parse_event_detail(row.get("detail")),
"created_at": created_at.isoformat() if isinstance(created_at, datetime) else "",
}
)
return JSONResponse({"events": output})
@app.get("/api/admin/audit/task-runs")
def list_audit_task_runs(
limit: int = 200,
request_code: str | None = None,
task: str | None = None,
ctx: AuthContext = Depends(require_auth),
) -> JSONResponse:
"""Return recorded background task runs for admin audit views."""
module = deps()
module._require_admin(ctx)
try:
rows = module.storage.list_task_runs(limit=limit, request_code=request_code, task=task)
except Exception:
raise HTTPException(status_code=502, detail="failed to load task runs")
output: list[dict[str, Any]] = []
for row in rows:
started_at = row.get("started_at")
finished_at = row.get("finished_at")
output.append(
{
"id": row.get("id"),
"request_code": row.get("request_code") or "",
"task": row.get("task") or "",
"status": row.get("status") or "",
"detail": module._parse_event_detail(row.get("detail")),
"started_at": started_at.isoformat() if isinstance(started_at, datetime) else "",
"finished_at": finished_at.isoformat() if isinstance(finished_at, datetime) else "",
"duration_ms": row.get("duration_ms"),
}
)
return JSONResponse({"task_runs": output})
@app.get("/api/admin/cluster/state")
def get_cluster_state(ctx: AuthContext = Depends(require_auth)) -> JSONResponse:
"""Return the latest cluster-state snapshot to authenticated administrators."""
module = deps()
module._require_admin(ctx)
snapshot = module.storage.latest_cluster_state()
if not snapshot:
raise HTTPException(status_code=404, detail="cluster state unavailable")
return JSONResponse(snapshot)
@app.get("/api/internal/cluster/state")
def get_cluster_state_internal() -> JSONResponse:
"""Return the latest cluster-state snapshot for trusted internal callers."""
module = deps()
snapshot = module.storage.latest_cluster_state()
if not snapshot:
raise HTTPException(status_code=404, detail="cluster state unavailable")
return JSONResponse(snapshot)
@app.post("/api/admin/access/requests/{username}/approve")
async def approve_access_request(
username: str,
request: Request,
ctx: AuthContext = Depends(require_auth),
) -> JSONResponse:
"""Approve a verified access request and start account provisioning."""
module = deps()
module._require_admin(ctx)
with task_context("admin.access.approve"):
payload = await module._read_json_payload(request)
allowed_flags = module._allowed_flag_groups()
flags = [flag for flag in module._flags_from_payload(payload) if flag in allowed_flags]
note = module._note_from_payload(payload)
decided_by = ctx.username or ""
try:
row = module.portal_db.fetchone(
"""
UPDATE access_requests
SET status = 'approved',
decided_at = NOW(),
decided_by = %s,
approval_flags = %s,
approval_note = %s
WHERE username = %s
AND status = 'pending'
AND email_verified_at IS NOT NULL
RETURNING request_code
""",
(decided_by or None, flags or None, note, username),
)
except Exception:
raise HTTPException(status_code=502, detail="failed to approve request")
if not row:
module.logger.info(
"access request approval ignored",
extra={"event": "access_request_approve", "actor": decided_by, "username": username, "status": "skipped"},
)
module._record_event(
"access_request_approve",
{
"actor": decided_by,
"username": username,
"status": "skipped",
},
)
return JSONResponse({"ok": True, "request_code": ""})
request_code = row.get("request_code") or ""
if request_code:
threading.Thread(
target=module.provisioning.provision_access_request,
args=(request_code,),
daemon=True,
).start()
module.logger.info(
"access request approved",
extra={
"event": "access_request_approve",
"actor": decided_by,
"username": username,
"request_code": request_code,
},
)
module._record_event(
"access_request_approve",
{
"actor": decided_by,
"username": username,
"request_code": request_code,
"status": "ok",
"flags": flags,
"note": note or "",
},
)
return JSONResponse({"ok": True, "request_code": request_code})
@app.post("/api/admin/access/requests/{username}/deny")
async def deny_access_request(
username: str,
request: Request,
ctx: AuthContext = Depends(require_auth),
) -> JSONResponse:
"""Deny a pending access request and record the administrator decision."""
module = deps()
module._require_admin(ctx)
with task_context("admin.access.deny"):
payload = await module._read_json_payload(request)
note = module._note_from_payload(payload)
decided_by = ctx.username or ""
try:
row = module.portal_db.fetchone(
"""
UPDATE access_requests
SET status = 'denied',
decided_at = NOW(),
decided_by = %s,
denial_note = %s
WHERE username = %s AND status = 'pending'
RETURNING request_code
""",
(decided_by or None, note, username),
)
except Exception:
raise HTTPException(status_code=502, detail="failed to deny request")
if not row:
module.logger.info(
"access request denial ignored",
extra={"event": "access_request_deny", "actor": decided_by, "username": username, "status": "skipped"},
)
module._record_event(
"access_request_deny",
{
"actor": decided_by,
"username": username,
"status": "skipped",
},
)
return JSONResponse({"ok": True, "request_code": ""})
module.logger.info(
"access request denied",
extra={
"event": "access_request_deny",
"actor": decided_by,
"username": username,
"request_code": row.get("request_code") or "",
},
)
module._record_event(
"access_request_deny",
{
"actor": decided_by,
"username": username,
"request_code": row.get("request_code") or "",
"status": "ok",
"note": note or "",
},
)
return JSONResponse({"ok": True, "request_code": row.get("request_code")})
@app.post("/api/access/requests/{request_code}/retry")
def retry_access_request(request_code: str) -> JSONResponse:
"""Reset failed provisioning tasks so an approved request can retry."""
module = deps()
code = (request_code or "").strip()
if not code:
raise HTTPException(status_code=400, detail="request_code is required")
if not module.keycloak_admin.ready():
raise HTTPException(status_code=503, detail="server not configured")
try:
row = module.portal_db.fetchone(
"SELECT status FROM access_requests WHERE request_code = %s",
(code,),
)
except Exception:
raise HTTPException(status_code=502, detail="failed to load request")
if not row:
raise HTTPException(status_code=404, detail="not found")
status = (row.get("status") or "").strip()
if status not in {"accounts_building", "approved"}:
raise HTTPException(status_code=409, detail="request not retryable")
try:
module.portal_db.execute(
"UPDATE access_requests SET provision_attempted_at = NULL WHERE request_code = %s",
(code,),
)
module.portal_db.execute(
"""
UPDATE access_request_tasks
SET status = 'pending',
detail = 'retry requested',
updated_at = NOW()
WHERE request_code = %s AND status = 'error'
""",
(code,),
)
except Exception:
raise HTTPException(status_code=502, detail="failed to update retry state")
threading.Thread(
target=module.provisioning.provision_access_request,
args=(code,),
daemon=True,
).start()
module._record_event(
"access_request_retry",
{
"request_code": code,
"status": "ok",
},
)
return JSONResponse({"ok": True, "request_code": code})

View File

@ -1,6 +1,5 @@
# path reason
ariadne/services/cluster_state.py split planned; service orchestration decomposition tracked in hygiene backlog
ariadne/app.py split planned; Flask app bootstrap/routes currently co-located
tests/test_provisioning.py test module split planned; broad provisioning coverage retained meanwhile
tests/test_services.py test module split planned; broad service contract coverage retained meanwhile
tests/test_app.py test module split planned; API coverage retained meanwhile

1 # path reason
2 ariadne/services/cluster_state.py split planned; service orchestration decomposition tracked in hygiene backlog
ariadne/app.py split planned; Flask app bootstrap/routes currently co-located
3 tests/test_provisioning.py test module split planned; broad provisioning coverage retained meanwhile
4 tests/test_services.py test module split planned; broad service contract coverage retained meanwhile
5 tests/test_app.py test module split planned; API coverage retained meanwhile