Spaces:
Runtime error
Runtime error
Claude Opus 5 (1M context)
feat(auth): inscription publique, connexion Google, et conversations rattachees a un compte
d21ea1e Download app/api/routes/admin.py from rahim237cm/jurix-api: direct link, hf CLI and curl.
- Browser
- Download file 9.45 kB
-
https://huggingface.co/spaces/rahim237cm/jurix-api/resolve/main/app/api/routes/admin.py
- Command line
-
hf download hf://spaces/rahim237cm/jurix-api/app/api/routes/admin.py
-
curl -L -o admin.py https://huggingface.co/spaces/rahim237cm/jurix-api/resolve/main/app/api/routes/admin.py
9.45 kB
| """ | |
| API routes for Admin operations. | |
| Endpoints: | |
| - GET /api/v1/admin/users - List users | |
| - POST /api/v1/admin/users - Create user | |
| - PUT /api/v1/admin/users/{id} - Update user | |
| - DELETE /api/v1/admin/users/{id} - Delete user | |
| - GET /api/v1/admin/system - System information | |
| Author: JuriX Development Team | |
| Date: 2026-01-11 | |
| """ | |
| import logging | |
| import platform | |
| from datetime import datetime | |
| from typing import Dict, List | |
| from fastapi import APIRouter, Depends, HTTPException, status | |
| from sqlalchemy import delete, or_, select | |
| from sqlalchemy.ext.asyncio import AsyncSession | |
| from app.core.auth import ( | |
| get_current_admin_user, | |
| get_current_superadmin_user, | |
| hash_password, | |
| ) | |
| from app.core.database import get_db, health_check_db | |
| from app.models.conversation import Conversation | |
| from app.models.user import User | |
| from app.schemas.user import UserCreate, UserResponse, UserUpdate | |
| from app.services.user_identity import normaliser_email | |
| logger = logging.getLogger(__name__) | |
| router = APIRouter(tags=["Admin"]) | |
| # ==================== SCHEMAS ==================== | |
| # | |
| # Les schemas UserCreate / UserUpdate / UserResponse etaient redefinis ici et | |
| # MASQUAIENT ceux de app/schemas/user.py. Le UserCreate local n'avait aucun champ | |
| # mot de passe : un utilisateur cree par cette API n'aurait jamais pu se | |
| # connecter. Ils sont desormais importes depuis la source unique. | |
| # | |
| # La liste _mock_users (en memoire, perdue a chaque redemarrage, non partagee | |
| # entre workers) a ete supprimee au profit du modele User et de la table users, | |
| # qui existaient tous deux depuis la migration 0b21fb6a3651 sans etre utilises. | |
| # ==================== USER MANAGEMENT ==================== | |
| async def list_users( | |
| skip: int = 0, | |
| limit: int = 100, | |
| db: AsyncSession = Depends(get_db), | |
| current_admin: User = Depends(get_current_admin_user), | |
| ) -> List[User]: | |
| """Liste les comptes (administrateurs).""" | |
| logger.info(f"📋 GET /admin/users - skip={skip}, limit={limit}") | |
| result = await db.execute( | |
| select(User).order_by(User.id).offset(skip).limit(min(limit, 500)) | |
| ) | |
| return list(result.scalars().all()) | |
| async def create_user( | |
| payload: UserCreate, | |
| db: AsyncSession = Depends(get_db), | |
| current_admin: User = Depends(get_current_admin_user), | |
| ) -> User: | |
| """ | |
| Crée un compte (administrateurs). | |
| Un administrateur ne peut pas créer de compte plus privilégié que le sien : | |
| sans cette règle, tout admin pourrait se hisser au rang de superadmin en | |
| créant un second compte. | |
| """ | |
| if payload.role in ("admin", "superadmin") and not current_admin.is_superadmin(): | |
| raise HTTPException( | |
| status_code=status.HTTP_403_FORBIDDEN, | |
| detail="Seul un superadmin peut créer un compte privilégié", | |
| ) | |
| existing = await db.execute( | |
| select(User).where( | |
| or_(User.email == payload.email, User.username == payload.username) | |
| ) | |
| ) | |
| if existing.scalar_one_or_none() is not None: | |
| raise HTTPException( | |
| status_code=status.HTTP_409_CONFLICT, | |
| detail="Un compte existe déjà avec cet email ou ce nom d'utilisateur", | |
| ) | |
| user = User( | |
| # Normalisee A L'ECRITURE : `_authenticate` cherche avec | |
| # `email.lower().strip()`. Sans cela, un compte cree avec « Jean@X.cm » | |
| # ne pouvait JAMAIS se connecter — le select ne le trouvait pas. | |
| email=normaliser_email(payload.email), | |
| username=payload.username, | |
| full_name=payload.full_name, | |
| role=payload.role, | |
| is_active=payload.is_active, | |
| hashed_password=hash_password(payload.password), | |
| ) | |
| db.add(user) | |
| await db.commit() | |
| await db.refresh(user) | |
| logger.info(f"✅ Compte créé : {user.email} (rôle {user.role})") | |
| return user | |
| async def update_user( | |
| user_id: int, | |
| payload: UserUpdate, | |
| db: AsyncSession = Depends(get_db), | |
| current_admin: User = Depends(get_current_admin_user), | |
| ) -> User: | |
| """ | |
| Met à jour un compte (administrateurs). | |
| Deux garde-fous : seul un superadmin peut modifier un rôle, et personne ne | |
| peut se retirer ses propres droits — sinon la première erreur de manipulation | |
| ferme définitivement l'accès à l'instance. | |
| """ | |
| user = ( | |
| await db.execute(select(User).where(User.id == user_id)) | |
| ).scalar_one_or_none() | |
| if user is None: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, detail=f"Compte {user_id} introuvable" | |
| ) | |
| changes = payload.model_dump(exclude_unset=True) | |
| if "role" in changes and not current_admin.is_superadmin(): | |
| raise HTTPException( | |
| status_code=status.HTTP_403_FORBIDDEN, | |
| detail="Seul un superadmin peut modifier un rôle", | |
| ) | |
| if user.id == current_admin.id and ( | |
| changes.get("role") not in (None, current_admin.role) | |
| or changes.get("is_active") is False | |
| ): | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="Impossible de réduire ses propres droits", | |
| ) | |
| password = changes.pop("password", None) | |
| if password: | |
| user.hashed_password = hash_password(password) | |
| for field, value in changes.items(): | |
| setattr(user, field, value) | |
| await db.commit() | |
| await db.refresh(user) | |
| logger.info(f"✏️ Compte mis à jour : {user.email}") | |
| return user | |
| async def delete_user( | |
| user_id: int, | |
| db: AsyncSession = Depends(get_db), | |
| current_admin: User = Depends(get_current_superadmin_user), | |
| ) -> None: | |
| """ | |
| Supprime un compte (superadministrateurs). | |
| Refuse la suppression de soi-même et celle du dernier superadmin actif : les | |
| deux mènent à une instance dont plus personne n'a les clés. | |
| """ | |
| user = ( | |
| await db.execute(select(User).where(User.id == user_id)) | |
| ).scalar_one_or_none() | |
| if user is None: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, detail=f"Compte {user_id} introuvable" | |
| ) | |
| if user.id == current_admin.id: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="Impossible de supprimer son propre compte", | |
| ) | |
| if user.role == "superadmin": | |
| remaining = ( | |
| await db.execute( | |
| select(User).where( | |
| User.role == "superadmin", | |
| User.is_active.is_(True), | |
| User.id != user.id, | |
| ) | |
| ) | |
| ).scalars().first() | |
| if remaining is None: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="Impossible de supprimer le dernier superadmin actif", | |
| ) | |
| # Suppression EXPLICITE des conversations, en remplacement de la cascade | |
| # ORM retiree de `User.conversations` (voir le commentaire du modele : la | |
| # cascade obligeait a charger toutes les conversations a chaque requete | |
| # authentifiee). Un `DELETE` en masse fait le meme travail sans rien | |
| # charger, et les messages suivent par la contrainte ON DELETE CASCADE. | |
| # | |
| # Sans cette ligne, la contrainte `ON DELETE SET NULL` de la colonne | |
| # laisserait les conversations orphelines en base apres la suppression du | |
| # compte — c'est-a-dire des questions juridiques conservees alors que leur | |
| # auteur a demande son effacement. | |
| await db.execute(delete(Conversation).where(Conversation.user_id == user.id)) | |
| await db.delete(user) | |
| await db.commit() | |
| logger.info(f"🗑️ Compte supprimé : {user.email}") | |
| return None | |
| # ==================== SYSTEM INFORMATION ==================== | |
| async def get_system_info( | |
| db: AsyncSession = Depends(get_db), | |
| current_admin: User = Depends(get_current_admin_user), | |
| ) -> Dict: | |
| """ | |
| Get system information (admin only). | |
| **Returns:** | |
| - Database status | |
| - System info | |
| - Service health | |
| - Version info | |
| **Requires:** Admin authentication | |
| """ | |
| logger.info("🖥️ GET /admin/system") | |
| # Check database health | |
| db_healthy = await health_check_db() | |
| return { | |
| "system": { | |
| "platform": platform.system(), | |
| "platform_version": platform.version(), | |
| "python_version": platform.python_version(), | |
| "hostname": platform.node(), | |
| }, | |
| "database": { | |
| "status": "healthy" if db_healthy else "unhealthy", | |
| "connected": db_healthy, | |
| }, | |
| "services": { | |
| "api": "running", | |
| "search": "running", | |
| "rag": "running", | |
| }, | |
| "version": { | |
| "api": "2.1.0", | |
| "build": "production", | |
| }, | |
| "timestamp": datetime.now().isoformat(), | |
| } | |
| # ==================== HEALTH CHECK ==================== | |
| async def health_check() -> Dict: | |
| """ | |
| Check admin service health. | |
| **Returns:** | |
| - Service status | |
| """ | |
| return { | |
| "service": "Admin", | |
| "status": "healthy", | |
| "timestamp": datetime.now().isoformat(), | |
| } | |