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

This commit is contained in:
darkorion
2026-10-13 09:09:53 +03:00
commit c298f5a830
247 changed files with 23200 additions and 0 deletions
+126
View File
@@ -0,0 +1,126 @@
import hashlib
import secrets
from datetime import datetime, timedelta, timezone
import bcrypt
import jwt
from fastapi import Depends, HTTPException, Request
from sqlalchemy.orm import Session
from . import config, mode
from .db import get_db
from .models import User
def hash_password(pw: str) -> str:
return bcrypt.hashpw(pw.encode(), bcrypt.gensalt()).decode()
def verify_password(pw: str, hashed: str) -> bool:
try:
return bcrypt.checkpw(pw.encode(), hashed.encode())
except ValueError:
return False
def make_token(user: User | int, demo: bool = False, rid: int | None = None) -> str:
"""Access-токен. demo=True — сессия в демо-БД (sub — id в демо-БД, rid — id того же человека в боевой БД,
None — чисто демонстрационная учётная запись)."""
exp = datetime.now(timezone.utc) + timedelta(minutes=config.ACCESS_TTL_MIN)
uid = user if isinstance(user, int) else user.id
claims = {"sub": str(uid), "exp": exp, "typ": "access", "mode": "demo" if demo else "real"}
if demo:
claims.update(rid=rid, ep=mode.State.epoch)
return jwt.encode(claims, config.SECRET_KEY, algorithm="HS256")
def new_refresh_token() -> tuple[str, str]:
"""(токен для клиента, sha256-хэш для БД)."""
raw = secrets.token_urlsafe(48)
return raw, hash_refresh(raw)
def hash_refresh(raw: str) -> str:
return hashlib.sha256(raw.encode()).hexdigest()
def current_user(request: Request, db: Session = Depends(get_db)) -> User:
auth = request.headers.get("authorization", "")
token = auth[7:] if auth.lower().startswith("bearer ") else None
if not token:
raise HTTPException(401, "Требуется авторизация")
try:
data = jwt.decode(token, config.SECRET_KEY, algorithms=["HS256"])
demo = data.get("mode", "real") == "demo"
rid = data.get("rid")
if demo:
# демо-сессия: демо-БД должна существовать, не пересоздаваться с выдачи токена,
# а человек — всё ещё числиться в демо (администратор мог его вывести)
if not db.info.get("demo") or data.get("ep") != mode.State.epoch or (rid is not None and not mode.user_in_demo(rid)):
raise ValueError("mode")
elif db.info.get("demo") or mode.user_in_demo(int(data["sub"])):
raise ValueError("mode") # человека перевели в демо — клиент обновит токен и получит демо-сессию
user = db.get(User, int(data["sub"]))
except Exception:
raise HTTPException(401, "Недействительный токен")
if not user or not user.is_active:
raise HTTPException(401, "Пользователь не найден или отключён")
request.state.real_user_id = rid if demo else user.id
request.state.demo = demo
return user
def is_engineer(u: User) -> bool:
return bool(u.position and u.position.kind == "engineer")
def is_manager(u: User) -> bool:
return bool(u.is_admin or u.is_moderator)
def is_root(u: User) -> bool:
"""Администратор по умолчанию (логин ADMIN_USERNAME, обычно admin): права администратора портала есть всегда."""
return bool(u.is_admin and u.username == config.ADMIN_USERNAME)
def is_portal_admin(u: User) -> bool:
"""Администратор портала: настройки сервера и ответы обратной связи."""
return bool(u.is_admin and (u.portal_admin or is_root(u)))
def require_portal_admin(u: User = Depends(current_user)) -> User:
if not is_portal_admin(u):
raise HTTPException(403, "Доступно только администраторам портала")
return u
def require_admin(u: User = Depends(current_user)) -> User:
"""Параметры сервера — только администратор."""
if not u.is_admin:
raise HTTPException(403, "Только для администратора")
return u
def require_manager(u: User = Depends(current_user)) -> User:
"""Администратор или модератор: пользователи, должности, графики."""
if not is_manager(u):
raise HTTPException(403, "Только для администратора или модератора")
return u
def require_engineer(u: User = Depends(current_user)) -> User:
"""Инженер, модератор или администратор."""
if not (is_manager(u) or is_engineer(u)):
raise HTTPException(403, "Недостаточно прав")
return u
def is_storekeeper(u: User) -> bool:
return bool(u.is_admin or u.is_storekeeper)
def require_storekeeper(u: User = Depends(current_user)) -> User:
"""Администратор склада инструмента или администратор: каталог, склады, выдача, заявки."""
if not is_storekeeper(u):
raise HTTPException(403, "Только для администратора склада")
return u