from __future__ import annotations import time import uuid from fastapi import APIRouter, Depends, HTTPException, Request, Response, status from sqlalchemy import select from sqlalchemy.orm import Session from app.api.deps import get_current_user, get_db, require_operations_manager from app.core.config import get_settings from app.core.security import SessionPayload, create_session_token, read_session_token from app.models.user import User from app.schemas import CurrentUser, DemoLoginRequest, DemoManifestOut from app.seed_loader import reset_and_seed from app.services.audit import record_audit_event from app.services.demo_manifest import build_demo_manifest, scenario_integrity_report router = APIRouter(prefix="/api/v1/demo", tags=["demo"]) settings = get_settings() @router.get("/manifest", response_model=DemoManifestOut) def demo_manifest(db: Session = Depends(get_db)) -> DemoManifestOut: # Deliberately unauthenticated: the demo-entry screen and the permanent demo badge # both need this before any session exists. Nothing here is sensitive — it's the same # honest "what is this demo" summary a logged-in user would see. return build_demo_manifest(db) @router.post("/login", response_model=CurrentUser) def demo_login( body: DemoLoginRequest, response: Response, db: Session = Depends(get_db) ) -> CurrentUser: public_ref = "USR-OPS" if body.role == "operations_manager" else "USR-EMP" user = db.scalar(select(User).where(User.public_ref == public_ref)) if user is None: raise LookupError("Demo users are missing; run the seed loader first.") token = create_session_token( SessionPayload( user_id=str(user.id), public_ref=user.public_ref, role=user.role, display_name=user.display_name, issued_at=int(time.time()), ) ) response.set_cookie( settings.session_cookie_name, token, httponly=True, samesite="lax", secure=settings.session_cookie_secure, max_age=settings.session_ttl_seconds, ) record_audit_event( db, actor_type="user", actor_id=user.id, actor_label=user.display_name, action="demo_login", entity_type="user", entity_id=user.id, ) db.commit() return CurrentUser(public_ref=user.public_ref, display_name=user.display_name, role=body.role) @router.get("/session", response_model=CurrentUser) def get_session( response: Response, user: CurrentUser = Depends(get_current_user) ) -> CurrentUser: # Never let the browser (or an intermediary) cache an authentication check — a stale # cached 200 here would keep showing a logged-out browser as authenticated. response.headers["Cache-Control"] = "no-store" return user @router.post("/logout") def demo_logout(request: Request, response: Response, db: Session = Depends(get_db)) -> dict: token = request.cookies.get(settings.session_cookie_name) payload = read_session_token(token) if token else None if payload is not None: record_audit_event( db, actor_type="user", actor_id=uuid.UUID(payload.user_id), actor_label=payload.display_name, action="demo_logout", entity_type="user", ) db.commit() response.delete_cookie(settings.session_cookie_name) return {"status": "logged_out"} @router.post("/reset") def demo_reset( response: Response, db: Session = Depends(get_db), user: CurrentUser = Depends(require_operations_manager), ) -> dict: if not settings.demo_allow_reset: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Demo reset is disabled on this deployment.", ) result = reset_and_seed(db) integrity = scenario_integrity_report(db) record_audit_event( db, actor_type="user", actor_label=user.display_name, action="demo_reset", entity_type="system", metadata={ "counts": result.counts, "anchor_date": result.anchor_date.isoformat(), "scenario_integrity": integrity, }, ) db.commit() response.delete_cookie(settings.session_cookie_name) return { "status": "reset", "counts": result.counts, "anchor_date": result.anchor_date.isoformat(), "seeded_at": result.seeded_at.isoformat(), "scenario_integrity": integrity, }