"""Setup router. Exposes the ``POST /api/setup`` endpoint for the one-time first-run configuration wizard. Once setup has been completed, subsequent calls return ``409 Conflict``. """ from __future__ import annotations import structlog from fastapi import APIRouter, HTTPException, status from app.dependencies import AppDep, DbDep, SettingsDep from app.models.setup import SetupRequest, SetupResponse, SetupStatusResponse, SetupTimezoneResponse from app.services import setup_service from app.utils.runtime_state import update_app_settings from app.utils.setup_state import is_setup_complete_cached, set_setup_complete_cache log: structlog.stdlib.BoundLogger = structlog.get_logger() router = APIRouter(prefix="/api/setup", tags=["setup"]) @router.get( "", response_model=SetupStatusResponse, summary="Check whether setup has been completed", ) async def get_setup_status(app: AppDep) -> SetupStatusResponse: """Return whether the initial setup wizard has been completed. Returns: :class:`~app.models.setup.SetupStatusResponse` with ``completed`` set to ``True`` if setup is done, ``False`` otherwise. """ done = is_setup_complete_cached(app) return SetupStatusResponse(completed=done) @router.post( "", response_model=SetupResponse, status_code=status.HTTP_201_CREATED, summary="Run the initial setup wizard", ) async def post_setup( app: AppDep, body: SetupRequest, db: DbDep, ) -> SetupResponse: """Persist the initial BanGUI configuration. Args: app: The FastAPI application instance. body: Setup request payload validated by Pydantic. db: Injected aiosqlite connection. Returns: :class:`~app.models.setup.SetupResponse` on success. Raises: HTTPException: 409 if setup has already been completed. """ if is_setup_complete_cached(app) or await setup_service.is_setup_complete(db): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Setup has already been completed.", ) await setup_service.run_setup( db, master_password=body.master_password, database_path=body.database_path, fail2ban_socket=body.fail2ban_socket, timezone=body.timezone, session_duration_minutes=body.session_duration_minutes, ) set_setup_complete_cache(app, True) update_app_settings( app, database_path=body.database_path, fail2ban_socket=body.fail2ban_socket, timezone=body.timezone, session_duration_minutes=body.session_duration_minutes, ) return SetupResponse() @router.get( "/timezone", response_model=SetupTimezoneResponse, summary="Return the configured IANA timezone", ) async def get_timezone(settings: SettingsDep) -> SetupTimezoneResponse: """Return the IANA timezone configured during the initial setup wizard. The frontend uses this to convert UTC timestamps to the local time zone chosen by the administrator. Returns: :class:`~app.models.setup.SetupTimezoneResponse` with ``timezone`` set to the stored IANA identifier (e.g. ``"UTC"`` or ``"Europe/Berlin"``), defaulting to ``"UTC"`` if unset. """ return SetupTimezoneResponse(timezone=settings.timezone)