"""Демонстрационный режим — персональный: в демо-БД работают только те, кто его включил (или кого включил администратор). Остальные в это время работают с боевыми данными. Подробности — app/demo.py, app/mode.py.""" from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel from sqlalchemy import select from sqlalchemy.orm import Session from .. import demo, mode, schemas as S from ..db import get_db, session_for from ..models import User from ..security import current_user, require_admin from .catalog import issue_tokens, tokens_for_real router = APIRouter(prefix="/demo", tags=["demo"]) class ConfirmIn(BaseModel): confirm: bool = False class ImpersonateIn(BaseModel): user_id: int class UserDemoIn(BaseModel): on: bool def real_admin(request: Request, u: User = Depends(require_admin)) -> User: """Администратор в боевой БД (запрос может прийти и из демо-сессии — тогда берём его настоящую учётную запись).""" rid = getattr(request.state, "real_user_id", None) if rid is None: raise HTTPException(403, "Демонстрационная учётная запись не может управлять демо-режимом") with session_for(False) as s: ru = s.get(User, rid) if not ru or not ru.is_admin or not ru.is_active: raise HTTPException(403, "Только для администратора") s.expunge(ru) return ru def _real_tokens(real_id: int, device: str) -> S.TokenOut: with session_for(False) as s: u = s.get(User, real_id) if not u or not u.is_active: raise HTTPException(404, "Пользователь не найден") return tokens_for_real(s, u, device) @router.get("/status") def status(): """Публичный: экрану входа — есть ли сейчас демо-учётные записи. Сами записи здесь не раскрываются.""" return {"active": mode.State.active} @router.get("/me") def my_demo(request: Request, u: User = Depends(current_user)): """Для баннера: в демо ли текущая сессия и можно ли из него выйти самому.""" return {"in_demo": bool(getattr(request.state, "demo", False)), "can_leave": getattr(request.state, "real_user_id", None) is not None} @router.get("") def info(request: Request, db: Session = Depends(get_db), admin: User = Depends(real_admin)): """Панель администратора: состояние демо-БД, кто сейчас в демо, демо-учётные записи.""" with session_for(False) as s: people = [{"id": x.id, "username": x.username, "full_name": x.full_name, "is_admin": x.is_admin, "in_demo": mode.user_in_demo(x.id)} for x in s.scalars(select(User).where(User.is_active.is_(True)).order_by(User.full_name, User.username))] out = {"active": mode.State.active, "since": mode.State.since, "in_demo": bool(getattr(request.state, "demo", False)), "users": people, "password": demo.DEMO_PASSWORD} if mode.State.active: with session_for(True) as ds: out["accounts"] = demo.demo_accounts(ds) return out @router.post("/enable", response_model=S.TokenOut) def enable(data: ConfirmIn, admin: User = Depends(real_admin)): """Войти в демо самому. Боевые данные не меняются, остальные пользователи продолжают работать как обычно.""" if not data.confirm: raise HTTPException(422, "Требуется подтверждение") try: demo.enable_for(admin, owner=admin) except demo.DemoError as e: raise HTTPException(409, str(e)) return _real_tokens(admin.id, "demo") @router.post("/leave", response_model=S.TokenOut) def leave(request: Request, u: User = Depends(current_user)): """Выйти из демо (любой пользователь, которого перевели в демо) и вернуться к боевым данным.""" rid = getattr(request.state, "real_user_id", None) if rid is None: raise HTTPException(409, "Это демонстрационная учётная запись — просто выйдите из неё") demo.disable_for(rid) return _real_tokens(rid, "web") @router.post("/users/{rid}") def set_user(rid: int, data: UserDemoIn, admin: User = Depends(real_admin)): """Включить или выключить демо другому пользователю (например, если кто-то случайно включил и не может выйти).""" with session_for(False) as s: target = s.get(User, rid) if not target or not target.is_active: raise HTTPException(404, "Пользователь не найден") s.expunge(target) if data.on: demo.enable_for(target, owner=admin) else: demo.disable_for(rid) return {"id": rid, "in_demo": mode.user_in_demo(rid)} @router.post("/reset") def reset(db: Session = Depends(get_db), admin: User = Depends(real_admin)): """Пересоздать тестовые данные (кто в демо — остаётся в демо).""" try: db.close() # освободить демо-БД до удаления файла (важно на Windows) demo.reset(admin) except demo.DemoError as e: raise HTTPException(409, str(e)) return {"ok": True} @router.post("/destroy") def destroy(db: Session = Depends(get_db), admin: User = Depends(real_admin)): """Удалить демо-данные целиком: все возвращаются к боевым данным.""" try: db.close() # освободить демо-БД до удаления файла (важно на Windows) demo.destroy() except demo.DemoError as e: raise HTTPException(409, str(e)) return {"ok": True} @router.post("/impersonate", response_model=S.TokenOut) def impersonate(data: ImpersonateIn, request: Request, admin: User = Depends(real_admin)): """Войти под демо-пользователем без пароля (показать роли). Только в демо-сессии; администраторов выбрать нельзя.""" if not getattr(request.state, "demo", False): raise HTTPException(409, "Сначала войдите в демо-режим") with session_for(True) as ds: target = ds.get(User, data.user_id) if not target or target.is_admin or not target.is_active: raise HTTPException(404, "Пользователь не найден") return issue_tokens(ds, target, "demo-impersonate")