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

71 lines
2.9 KiB
Python

from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from app.deps import DB, CurrentUser, require
from app.models import Role, User
from app.permissions import PERMISSIONS
from app.schemas import RoleIn, RoleOut
router = APIRouter(prefix="/api/roles", tags=["roles"])
RoleAdmin = Annotated[User, Depends(require("roles.manage"))]
def _check_perms(perms: list[str]) -> list[str]:
unknown = [p for p in perms if p not in PERMISSIONS]
if unknown:
raise HTTPException(status.HTTP_400_BAD_REQUEST, f"Неизвестные права: {', '.join(unknown)}")
return sorted(set(perms))
@router.get("/permissions")
async def list_permissions(_: CurrentUser) -> dict[str, str]:
return PERMISSIONS
@router.get("", response_model=list[RoleOut])
async def list_roles(user: CurrentUser, db: DB) -> list[Role]:
# role list is needed by the user admin page too
if not {"roles.manage", "users.manage"} & set(user.permissions):
raise HTTPException(status.HTTP_403_FORBIDDEN, "Нет прав")
return list((await db.scalars(select(Role).order_by(Role.id))).all())
@router.post("", response_model=RoleOut, status_code=201)
async def create_role(body: RoleIn, _: RoleAdmin, db: DB) -> Role:
if await db.scalar(select(Role.id).where(Role.name == body.name)):
raise HTTPException(status.HTTP_409_CONFLICT, "Роль с таким именем уже есть")
role = Role(name=body.name, permissions=_check_perms(body.permissions))
db.add(role)
await db.commit()
return role
@router.put("/{role_id}", response_model=RoleOut)
async def update_role(role_id: int, body: RoleIn, _: RoleAdmin, db: DB) -> Role:
role = await db.get(Role, role_id)
if not role:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Роль не найдена")
if role.name == "admin":
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Роль admin нельзя изменить")
if role.is_system and body.name != role.name:
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Системную роль нельзя переименовать")
role.name = body.name
role.permissions = _check_perms(body.permissions)
await db.commit()
return role
@router.delete("/{role_id}", status_code=204)
async def delete_role(role_id: int, _: RoleAdmin, db: DB) -> None:
role = await db.get(Role, role_id)
if not role:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Роль не найдена")
if role.is_system:
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Системную роль нельзя удалить")
if await db.scalar(select(User.id).where(User.role_id == role_id).limit(1)):
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Роль назначена пользователям")
await db.delete(role)
await db.commit()