All checks were successful
PostgreSQL Compatibility Matrix / PG14 smoke (pull_request) Successful in 12s
PostgreSQL Compatibility Matrix / PG15 smoke (pull_request) Successful in 11s
PostgreSQL Compatibility Matrix / PG16 smoke (pull_request) Successful in 9s
PostgreSQL Compatibility Matrix / PG17 smoke (pull_request) Successful in 10s
PostgreSQL Compatibility Matrix / PG18 smoke (pull_request) Successful in 11s
This commit introduces optional `first_name` and `last_name` fields to the user model, including database migrations, backend, and frontend support. It enhances user profiles, updates user creation and editing flows, and refines the UI to display full names where available.
79 lines
3.2 KiB
Python
79 lines
3.2 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from app.core.db import get_db
|
|
from app.core.deps import require_roles
|
|
from app.core.security import hash_password
|
|
from app.models.models import User
|
|
from app.schemas.user import UserCreate, UserOut, UserUpdate
|
|
from app.services.audit import write_audit_log
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("", response_model=list[UserOut])
|
|
async def list_users(admin: User = Depends(require_roles("admin")), db: AsyncSession = Depends(get_db)) -> list[UserOut]:
|
|
users = (await db.scalars(select(User).order_by(User.id.asc()))).all()
|
|
_ = admin
|
|
return [UserOut.model_validate(user) for user in users]
|
|
|
|
|
|
@router.post("", response_model=UserOut, status_code=status.HTTP_201_CREATED)
|
|
async def create_user(payload: UserCreate, admin: User = Depends(require_roles("admin")), db: AsyncSession = Depends(get_db)) -> UserOut:
|
|
exists = await db.scalar(select(User).where(User.email == payload.email))
|
|
if exists:
|
|
raise HTTPException(status_code=409, detail="Email already exists")
|
|
user = User(
|
|
email=payload.email,
|
|
first_name=payload.first_name,
|
|
last_name=payload.last_name,
|
|
password_hash=hash_password(payload.password),
|
|
role=payload.role,
|
|
)
|
|
db.add(user)
|
|
await db.commit()
|
|
await db.refresh(user)
|
|
await write_audit_log(db, "admin.user.create", admin.id, {"created_user_id": user.id})
|
|
return UserOut.model_validate(user)
|
|
|
|
|
|
@router.put("/{user_id}", response_model=UserOut)
|
|
async def update_user(
|
|
user_id: int,
|
|
payload: UserUpdate,
|
|
admin: User = Depends(require_roles("admin")),
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> UserOut:
|
|
user = await db.scalar(select(User).where(User.id == user_id))
|
|
if not user:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
update_data = payload.model_dump(exclude_unset=True)
|
|
next_email = update_data.get("email")
|
|
if next_email and next_email != user.email:
|
|
existing = await db.scalar(select(User).where(User.email == next_email))
|
|
if existing and existing.id != user.id:
|
|
raise HTTPException(status_code=409, detail="Email already exists")
|
|
if "password" in update_data:
|
|
raw_password = update_data.pop("password")
|
|
if raw_password:
|
|
user.password_hash = hash_password(raw_password)
|
|
for key, value in update_data.items():
|
|
setattr(user, key, value)
|
|
await db.commit()
|
|
await db.refresh(user)
|
|
await write_audit_log(db, "admin.user.update", admin.id, {"updated_user_id": user.id})
|
|
return UserOut.model_validate(user)
|
|
|
|
|
|
@router.delete("/{user_id}")
|
|
async def delete_user(user_id: int, admin: User = Depends(require_roles("admin")), db: AsyncSession = Depends(get_db)) -> dict:
|
|
if user_id == admin.id:
|
|
raise HTTPException(status_code=400, detail="Cannot delete yourself")
|
|
user = await db.scalar(select(User).where(User.id == user_id))
|
|
if not user:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
await db.delete(user)
|
|
await db.commit()
|
|
await write_audit_log(db, "admin.user.delete", admin.id, {"deleted_user_id": user_id})
|
|
return {"status": "deleted"}
|