from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Request from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import Settings from app.core.deps import get_app_settings, get_db from app.models.challenge import Challenge from app.schemas.api import ( ChallengeApprovalRequest, ChallengeCreateRequest, ChallengeOut, DenyRequest, EnrollmentFinishRequest, EnrollmentFinishResponse, EnrollmentStartRequest, EnrollmentStartResponse, ) from app.services.challenges import approve_challenge, create_challenge, deny_challenge, expire_if_needed from app.services.enrollment import finish_enrollment, start_enrollment from app.services.push import PushService router = APIRouter(prefix="/api", tags=["api"]) @router.post("/enroll/start", response_model=EnrollmentStartResponse) async def enroll_start( body: EnrollmentStartRequest, request: Request, session: AsyncSession = Depends(get_db), settings: Settings = Depends(get_app_settings), ): enrollment, qr_payload, qr_b64 = await start_enrollment( session, settings, username=body.username, display_name=body.display_name, email=body.email, ip_address=request.client.host if request.client else None, ) await session.commit() return EnrollmentStartResponse( enrollment_id=enrollment.id, qr_payload=qr_payload, qr_png_base64=qr_b64, expires_at=enrollment.expires_at, ) @router.post("/enroll/finish", response_model=EnrollmentFinishResponse) async def enroll_finish( body: EnrollmentFinishRequest, request: Request, session: AsyncSession = Depends(get_db), ): device = await finish_enrollment( session, enrollment_token=body.enrollment_token, device_name=body.device_name, public_key_pem=body.public_key_pem, public_key_alg=body.public_key_alg, fcm_token=body.fcm_token, app_version=body.app_version, attestation=body.attestation, ip_address=request.client.host if request.client else None, ) if not device: raise HTTPException(status_code=400, detail="Invalid or expired enrollment") await session.commit() return EnrollmentFinishResponse(device_id=device.id, user_id=device.user_id) @router.post("/challenges", response_model=ChallengeOut) async def challenges_create( body: ChallengeCreateRequest, session: AsyncSession = Depends(get_db), settings: Settings = Depends(get_app_settings), ): challenge = await create_challenge( session, settings, PushService(settings), username=body.username, relying_party=body.relying_party, requester_ip=body.requester_ip, location=body.location, ttl_seconds=body.ttl_seconds, oidc_state=body.oidc_state, ) if not challenge: raise HTTPException(status_code=404, detail="No active enrolled device for user") await session.commit() return challenge @router.get("/challenges/{challenge_id}", response_model=ChallengeOut) async def challenges_get(challenge_id: UUID, session: AsyncSession = Depends(get_db)): challenge = await session.get(Challenge, challenge_id) if not challenge: raise HTTPException(status_code=404, detail="Challenge not found") await expire_if_needed(session, challenge) await session.commit() return challenge @router.post("/challenges/{challenge_id}/approve") async def challenges_approve( challenge_id: UUID, body: ChallengeApprovalRequest, request: Request, session: AsyncSession = Depends(get_db), ): challenge = await session.get(Challenge, challenge_id) if not challenge: raise HTTPException(status_code=404, detail="Challenge not found") ok, reason = await approve_challenge( session, challenge=challenge, device_id=body.device_id, payload=body.payload, signature=body.signature, ip_address=request.client.host if request.client else None, ) await session.commit() if not ok: raise HTTPException(status_code=400, detail=reason) return {"status": "approved"} @router.post("/challenges/{challenge_id}/deny") async def challenges_deny( challenge_id: UUID, body: DenyRequest, request: Request, session: AsyncSession = Depends(get_db), ): challenge = await session.get(Challenge, challenge_id) if not challenge: raise HTTPException(status_code=404, detail="Challenge not found") ok, reason = await deny_challenge( session, challenge=challenge, device_id=body.device_id, reason=body.reason, ip_address=request.client.host if request.client else None, ) await session.commit() if not ok: raise HTTPException(status_code=400, detail=reason) return {"status": "denied"}