"""Initial install bootstrap and authenticated setup wizard endpoints.""" from inspect import isawaitable from fastapi import APIRouter, Depends, HTTPException, Request, Response from pydantic import Field, SecretStr from ..api_models import COMMON_ERROR_RESPONSES, StrictRequest from ..auth import _extract_client_ip, require_admin from ..services import setup as setup_service from ..installation_origin import normalize_application_origin from ..services.request_origins import can_claim_initial_origin router = APIRouter(prefix="/setup", tags=["setup"], responses=COMMON_ERROR_RESPONSES) class BootstrapRequest(StrictRequest): setup_token: SecretStr = Field(min_length=1, max_length=1024) username: str = Field(min_length=1, max_length=100) password: SecretStr = Field(min_length=1, max_length=1024) application_url: str | None = Field(default=None, max_length=2048) class SetupProgress(StrictRequest): step: setup_service.SetupStep @router.get("/status") def public_status(response: Response) -> dict: response.headers["Cache-Control"] = "no-store" return setup_service.get_public_setup_status() @router.post("/bootstrap", status_code=201) def bootstrap(payload: BootstrapRequest, request: Request) -> dict: status = setup_service.get_public_setup_status() if not status["needs_admin"]: raise HTTPException(status_code=409, detail="Initial administrator setup is no longer available.") retry_after = setup_service.consume_bootstrap_attempt(_extract_client_ip(request)) if retry_after is not None: raise HTTPException( status_code=429, detail="Too many setup attempts. Try again later.", headers={"Retry-After": str(retry_after)}, ) try: application_url = payload.application_url if application_url is not None: application_url = normalize_application_origin(application_url) origin = request.headers.get("origin", "") if not origin or application_url != normalize_application_origin(origin): raise HTTPException(status_code=403, detail="The site address must match the address open in your browser.") elif can_claim_initial_origin(): raise HTTPException(status_code=400, detail="Confirm the application URL to create the administrator.") setup_service.bootstrap_administrator( payload.setup_token.get_secret_value(), payload.username, payload.password.get_secret_value(), application_url=application_url, ) except setup_service.InvalidSetupTokenError as exc: raise HTTPException(status_code=403, detail=str(exc)) from exc except setup_service.SetupUnavailableError as exc: raise HTTPException(status_code=409, detail=str(exc)) from exc except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc return {"status": "created", "username": payload.username.strip()} @router.get("/state", dependencies=[Depends(require_admin)]) def get_state() -> dict: return setup_service.get_setup_state() @router.put("/state", dependencies=[Depends(require_admin)]) def update_state(payload: SetupProgress) -> dict: return setup_service.update_setup_step(payload.step) @router.post("/complete", dependencies=[Depends(require_admin)]) async def finish_setup(request: Request) -> dict: try: state = setup_service.complete_setup() except setup_service.SetupUnavailableError as exc: raise HTTPException(status_code=409, detail=str(exc)) from exc # Startup owns worker lifecycle. Its callback must be idempotent so retries # after a network interruption cannot start duplicate import/automation jobs. callback = getattr(request.app.state, "on_setup_complete", None) if callback is not None: result = callback() if isawaitable(result): await result return state