"""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, status from app.dependencies import AppDep, SettingsDep, SettingsServiceContextDep from app.exceptions import SetupAlreadyCompleteError 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/v1/setup", tags=["setup"]) @router.get( "", response_model=SetupStatusResponse, summary="Check whether setup has been completed", responses={ 200: {"description": "Setup status returned", "model": SetupStatusResponse}, }, ) 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", responses={ 201: {"description": "Setup completed successfully", "model": SetupResponse}, 400: {"description": "Validation error in request body"}, 409: {"description": "Setup already completed"}, }, ) async def post_setup( app: AppDep, body: SetupRequest, settings_ctx: SettingsServiceContextDep, ) -> SetupResponse: """Persist the initial BanGUI configuration. Args: app: The FastAPI application instance. body: Setup request payload validated by Pydantic. settings_ctx: Settings service context containing db and repository. Returns: :class:`~app.models.setup.SetupResponse` on success. Raises: SetupAlreadyCompleteError: if setup has already been completed. """ if is_setup_complete_cached(app) or await setup_service.is_setup_complete(settings_ctx.db): raise SetupAlreadyCompleteError() await setup_service.run_setup( settings_ctx.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", responses={ 200: {"description": "Timezone returned", "model": SetupTimezoneResponse}, }, ) 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)