# ============================================================ # routers/admin.py — Gestion des comptes et journal d'audit # # Réservé au rôle Admin (403 sinon), via require_admin. # ============================================================ from typing import Optional from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel from auth import hash_password, require_admin from config import get_cursor from domain import AuditAction, UserRole from helpers import journaliser, rows_to_list router = APIRouter(tags=["Admin"]) class UserCreate(BaseModel): username: str email: str password: str role: UserRole class UserUpdate(BaseModel): email: Optional[str] = None role: Optional[UserRole] = None actif: Optional[bool] = None class PasswordReset(BaseModel): password: str @router.get("/admin/users") def list_users(admin: dict = Depends(require_admin)): with get_cursor() as cursor: cursor.execute( "SELECT id_user, username, email, role, actif, created_at, last_login " "FROM [USER] ORDER BY id_user" ) return rows_to_list(cursor, cursor.fetchall()) @router.post("/admin/users", status_code=status.HTTP_201_CREATED) def create_user(body: UserCreate, admin: dict = Depends(require_admin)): # Le rôle est validé par Pydantic via l'enum UserRole (422 si invalide). with get_cursor() as cursor: cursor.execute( "INSERT INTO [USER] (username, email, password_hash, role) VALUES (?, ?, ?, ?)", body.username, body.email, hash_password(body.password), body.role.value ) journaliser(cursor, admin, AuditAction.CREATE_USER, body.username) return {"status": "created", "username": body.username} @router.put("/admin/users/{id_user}") def update_user(id_user: int, body: UserUpdate, admin: dict = Depends(require_admin)): sets, params = [], [] if body.email is not None: sets.append("email = ?"); params.append(body.email) if body.role is not None: sets.append("role = ?"); params.append(body.role.value) if body.actif is not None: sets.append("actif = ?"); params.append(1 if body.actif else 0) if not sets: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Aucun champ à modifier" ) params.append(id_user) with get_cursor() as cursor: cursor.execute(f"UPDATE [USER] SET {', '.join(sets)} WHERE id_user = ?", *params) journaliser(cursor, admin, AuditAction.UPDATE_USER, str(id_user)) return {"status": "updated", "id_user": id_user} @router.delete("/admin/users/{id_user}") def delete_user(id_user: int, admin: dict = Depends(require_admin)): """Suppression douce (actif = 0).""" with get_cursor() as cursor: cursor.execute("UPDATE [USER] SET actif = 0 WHERE id_user = ?", id_user) journaliser(cursor, admin, AuditAction.DELETE_USER, str(id_user)) return {"status": "deactivated", "id_user": id_user} @router.post("/admin/users/{id_user}/reset-password") def reset_password(id_user: int, body: PasswordReset, admin: dict = Depends(require_admin)): with get_cursor() as cursor: cursor.execute("UPDATE [USER] SET password_hash = ? WHERE id_user = ?", hash_password(body.password), id_user) journaliser(cursor, admin, AuditAction.RESET_PASSWORD, str(id_user)) return {"status": "password_reset", "id_user": id_user} @router.get("/admin/journal") def get_journal(limit: int = 200, admin: dict = Depends(require_admin)): with get_cursor() as cursor: cursor.execute( "SELECT TOP (?) date_action, username, action, detail, ip " "FROM JOURNAL_AUDIT ORDER BY date_action DESC", limit ) return rows_to_list(cursor, cursor.fetchall())