Files
pgames-manager/backend/app/routers/auth.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

79 lines
2.9 KiB
Python

from fastapi import APIRouter, HTTPException, Response, status
from sqlalchemy import select
from app.config import get_settings
from app.deps import DB, SessionUser
from app.models import User
from app.schemas import ChangeCredentialsIn, LoginIn, MeOut
from app.security import (
COOKIE_NAME,
create_token,
hash_password,
validate_new_password,
validate_username,
verify_password,
)
router = APIRouter(prefix="/api/auth", tags=["auth"])
def _set_cookie(response: Response, user: User) -> None:
settings = get_settings()
response.set_cookie(
COOKIE_NAME,
create_token(user.id, user.token_version),
max_age=settings.session_ttl_hours * 3600,
httponly=True,
samesite="lax",
secure=settings.cookie_secure,
)
@router.post("/login", response_model=MeOut)
async def login(body: LoginIn, response: Response, db: DB) -> User:
user = await db.scalar(select(User).where(User.username == body.username))
if not user or not user.is_active or not verify_password(user.password_hash, body.password):
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Неверный логин или пароль")
_set_cookie(response, user)
return user
@router.post("/logout", status_code=204)
async def logout(response: Response) -> None:
response.delete_cookie(COOKIE_NAME)
@router.get("/me", response_model=MeOut)
async def me(user: SessionUser) -> User:
return user
@router.post("/change-credentials", response_model=MeOut)
async def change_credentials(
body: ChangeCredentialsIn, response: Response, user: SessionUser, db: DB
) -> User:
if not verify_password(user.password_hash, body.current_password):
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Текущий пароль неверен")
if body.new_password == body.current_password:
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Новый пароль совпадает со старым")
if err := validate_new_password(body.new_password):
raise HTTPException(status.HTTP_400_BAD_REQUEST, err)
new_username = (body.new_username or "").strip()
if user.must_change_username and (not new_username or new_username == user.username):
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Необходимо задать новый логин")
if new_username and new_username != user.username:
if err := validate_username(new_username):
raise HTTPException(status.HTTP_400_BAD_REQUEST, err)
if await db.scalar(select(User.id).where(User.username == new_username)):
raise HTTPException(status.HTTP_409_CONFLICT, "Логин уже занят")
user.username = new_username
user.password_hash = hash_password(body.new_password)
user.must_change_password = False
user.must_change_username = False
user.token_version += 1
await db.commit()
_set_cookie(response, user)
return user