Портал технической службы: наряды и чек-листы, задачи, отказы и неисправности, анализ надёжности, карты, склады, приложение для Android

This commit is contained in:
darkorion
2026-10-13 09:09:53 +03:00
commit 67e411eea8
246 changed files with 23152 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
"""Демонстрационный режим — персональный: в демо-БД работают только те, кто его включил (или кого включил администратор).
Остальные в это время работают с боевыми данными. Подробности — 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")