Replaced all inline error messages with the standardized `api_error` helper for consistent error response formatting. This improves clarity, maintainability, and ensures uniform error structures across the application. Updated logging for collector failures to include error class and switched to warning level for target unreachable scenarios.
80 lines
3.4 KiB
Python
80 lines
3.4 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.errors import api_error
|
|
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=api_error("email_exists", "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=api_error("user_not_found", "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=api_error("email_exists", "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=api_error("cannot_delete_self", "Cannot delete yourself"))
|
|
user = await db.scalar(select(User).where(User.id == user_id))
|
|
if not user:
|
|
raise HTTPException(status_code=404, detail=api_error("user_not_found", "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"}
|