Files
pgames-manager/backend/app/security.py
T
mrleo1nidandClaude Opus 5.5 364b312064 Initial MVP: local visual novel library with Ren'Py → PGVN converter
- FastAPI backend: admin bootstrap with forced credential change, admin-created
  users with one-time generated passwords, roles as permission sets
- Games and versions with content-addressed blob storage; forks copy only the
  manifest, so mods share assets with the original
- Worker: archive/RPA extraction, unrpyc for .rpyc, own .rpy parser, Python
  subset translated via ast, media converted to WebP/Opus/WebM
- React panel (library search, game page, upload, users, roles) and a
  framework-free web player with saves, history, skip/auto, transitions
- docker compose: postgres, backend, worker, nginx on a single HTTP port

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:41:32 +03:00

61 lines
1.8 KiB
Python

import secrets
import string
from datetime import UTC, datetime, timedelta
import jwt
from argon2 import PasswordHasher
from argon2.exceptions import InvalidHashError, VerifyMismatchError
from app.config import get_settings
COOKIE_NAME = "pgames_session"
_hasher = PasswordHasher()
_ALPHABET = string.ascii_letters + string.digits
def hash_password(password: str) -> str:
return _hasher.hash(password)
def verify_password(password_hash: str, password: str) -> bool:
try:
return _hasher.verify(password_hash, password)
except (VerifyMismatchError, InvalidHashError):
return False
def generate_password(length: int = 14) -> str:
return "".join(secrets.choice(_ALPHABET) for _ in range(length))
def create_token(user_id: int, token_version: int) -> str:
settings = get_settings()
payload = {
"sub": str(user_id),
"tv": token_version,
"exp": datetime.now(UTC) + timedelta(hours=settings.session_ttl_hours),
}
return jwt.encode(payload, settings.secret_key, algorithm="HS256")
def decode_token(token: str) -> tuple[int, int] | None:
try:
payload = jwt.decode(token, get_settings().secret_key, algorithms=["HS256"])
return int(payload["sub"]), int(payload["tv"])
except (jwt.PyJWTError, KeyError, ValueError):
return None
def validate_new_password(password: str) -> str | None:
if len(password) < 8:
return "Пароль должен быть не короче 8 символов"
return None
def validate_username(username: str) -> str | None:
if not 3 <= len(username) <= 64:
return "Логин должен быть от 3 до 64 символов"
if not all(c.isalnum() or c in "._-" for c in username):
return "Логин может содержать буквы, цифры и . _ -"
return None