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>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-25 02:41:32 +03:00
commit 364b312064
72 files changed
+9707

No files matched your search

+53
View File
@@ -0,0 +1,53 @@
from collections.abc import Callable
from typing import Annotated
from fastapi import Cookie, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.db import get_session
from app.models import User
from app.security import COOKIE_NAME, decode_token
DB = Annotated[AsyncSession, Depends(get_session)]
async def _load_user(db: AsyncSession, token: str | None) -> User:
if not token or not (decoded := decode_token(token)):
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Не авторизован")
user_id, token_version = decoded
user = await db.get(User, user_id)
if not user or not user.is_active or user.token_version != token_version:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Сессия недействительна")
return user
async def session_user(
db: DB, token: Annotated[str | None, Cookie(alias=COOKIE_NAME)] = None
) -> User:
"""Authenticated user, even if they still have to change credentials."""
return await _load_user(db, token)
async def current_user(user: Annotated[User, Depends(session_user)]) -> User:
"""Authenticated user who has finished the forced credential change."""
if user.must_change_password or user.must_change_username:
raise HTTPException(status.HTTP_403_FORBIDDEN, "Необходимо сменить учётные данные")
return user
SessionUser = Annotated[User, Depends(session_user)]
CurrentUser = Annotated[User, Depends(current_user)]
def require(*perms: str) -> Callable:
async def checker(user: CurrentUser) -> User:
missing = [p for p in perms if p not in user.permissions]
if missing:
raise HTTPException(status.HTTP_403_FORBIDDEN, f"Нет прав: {', '.join(missing)}")
return user
return checker
def has_perm(user: User, perm: str) -> bool:
return perm in user.permissions