81 lines
3.0 KiB
Python
81 lines
3.0 KiB
Python
"""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
|
|
|
|
|
|
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)
|
|
|
|
|
|
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:
|
|
setup_service.bootstrap_administrator(
|
|
payload.setup_token.get_secret_value(), payload.username, payload.password.get_secret_value()
|
|
)
|
|
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
|