"""First-boot setup endpoints. GET /setup/status — public, is_first_run + JWT secret presence POST /setup/create-admin — public, only allowed when is_first_run is true GET /setup/recovery — localhost-only password recovery probe """ from fastapi import APIRouter, Depends, HTTPException, Request from sqlalchemy.ext.asyncio import AsyncSession from mac.database import get_db from mac.schemas.setup import CreateAdminRequest, CreateAdminResponse, SetupStatus from mac.services import setup_service, updater router = APIRouter(prefix="/setup", tags=["Setup"]) @router.get("/status", response_model=SetupStatus) async def setup_status(db: AsyncSession = Depends(get_db)): return SetupStatus( is_first_run=await setup_service.is_first_run(db), has_jwt_secret=await setup_service.has_jwt_secret(db), version=updater.get_current_version(), ) @router.post("/create-admin", response_model=CreateAdminResponse) async def create_admin( body: CreateAdminRequest, db: AsyncSession = Depends(get_db), ): user, token, error = await setup_service.create_founder_admin( db, name=body.name, email=body.email, password=body.password, ) if error or not user or not token: raise HTTPException(status_code=409, detail={ "code": "setup_closed", "message": error or "Setup already completed.", }) return CreateAdminResponse( access_token=token, token_type="bearer", user={ "id": user.id, "name": user.name, "email": user.email, "role": user.role, "is_founder": user.is_founder, "roll_number": user.roll_number, }, ) @router.get("/recovery") async def recovery(request: Request): """Localhost-only password recovery surface. Refuses non-loopback callers. Real recovery flow is a Session 3 deliverable; this session just proves the gating works.""" client_host = request.client.host if request.client else "" if client_host not in ("127.0.0.1", "::1", "localhost"): raise HTTPException(status_code=403, detail={ "code": "localhost_required", "message": "Recovery is only accessible from the host machine (127.0.0.1).", }) return { "ok": True, "message": "Localhost recovery endpoint is reachable. Full flow lands in Session 3.", }