Files
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

89 lines
3.3 KiB
Python

from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from app.deps import DB, require
from app.models import Role, User
from app.schemas import UserCreateIn, UserOut, UserUpdateIn, UserWithPasswordOut
from app.security import generate_password, hash_password, validate_username
router = APIRouter(prefix="/api/users", tags=["users"])
Admin = Annotated[User, Depends(require("users.manage"))]
async def _get_user(db: DB, user_id: int) -> User:
user = await db.get(User, user_id)
if not user:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Пользователь не найден")
return user
async def _get_role(db: DB, role_id: int) -> Role:
role = await db.get(Role, role_id)
if not role:
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Роль не найдена")
return role
@router.get("", response_model=list[UserOut])
async def list_users(_: Admin, db: DB) -> list[User]:
return list((await db.scalars(select(User).order_by(User.username))).all())
@router.post("", response_model=UserWithPasswordOut, status_code=201)
async def create_user(body: UserCreateIn, _: Admin, db: DB) -> dict:
username = body.username.strip()
if err := validate_username(username):
raise HTTPException(status.HTTP_400_BAD_REQUEST, err)
if await db.scalar(select(User.id).where(User.username == username)):
raise HTTPException(status.HTTP_409_CONFLICT, "Логин уже занят")
role = await _get_role(db, body.role_id)
password = generate_password()
user = User(
username=username,
password_hash=hash_password(password),
role_id=role.id,
must_change_password=True,
)
db.add(user)
await db.commit()
await db.refresh(user)
return {"user": user, "password": password}
@router.patch("/{user_id}", response_model=UserOut)
async def update_user(user_id: int, body: UserUpdateIn, admin: Admin, db: DB) -> User:
user = await _get_user(db, user_id)
if user.id == admin.id and (body.is_active is False or body.role_id not in (None, user.role_id)):
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Нельзя отключить или понизить самого себя")
if body.role_id is not None:
user.role_id = (await _get_role(db, body.role_id)).id
if body.is_active is not None and body.is_active != user.is_active:
user.is_active = body.is_active
user.token_version += 1
await db.commit()
await db.refresh(user)
return user
@router.post("/{user_id}/reset-password", response_model=UserWithPasswordOut)
async def reset_password(user_id: int, _: Admin, db: DB) -> dict:
user = await _get_user(db, user_id)
password = generate_password()
user.password_hash = hash_password(password)
user.must_change_password = True
user.token_version += 1
await db.commit()
await db.refresh(user)
return {"user": user, "password": password}
@router.delete("/{user_id}", status_code=204)
async def delete_user(user_id: int, admin: Admin, db: DB) -> None:
user = await _get_user(db, user_id)
if user.id == admin.id:
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Нельзя удалить самого себя")
await db.delete(user)
await db.commit()