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()