- 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>
61 lines
1.8 KiB
Python
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
|