from fastapi import APIRouter, HTTPException, status, Depends from bson import ObjectId import pymongo from datetime import datetime, timedelta import app.config as config from app.models import User, HTTPError from app.models.config import SystemConfig, DBConfig from app.routes.auth import users_collection from app.routes.utils import get_admin_user from app.dto import UserAdminDTO # Database setup client = pymongo.MongoClient(config.MONGODB_URL, username=config.MONGODB_USERNAME, password=config.MONGODB_PASSWORD) db = client[config.MONGODB_DATABASE] pins_collection = db["pins"] images_collection = db["images"] friends_collection = db["friends"] pin_permissions_collection = db["pin_permissions"] config_collection = db["config"] admin_router = APIRouter( prefix="/admin", tags=["Admin"] ) def serialize_mongo_doc(doc): """Convertit un document MongoDB en dictionnaire sérialisable""" if doc is None: return None result = {} for key, value in doc.items(): if isinstance(value, ObjectId): result[key] = str(value) elif isinstance(value, dict): result[key] = serialize_mongo_doc(value) elif isinstance(value, list): result[key] = [serialize_mongo_doc(item) if isinstance(item, dict) else str(item) if isinstance(item, ObjectId) else item for item in value] else: result[key] = value return result def load_config(): """Charge la configuration depuis la base de données ou utilise les valeurs par défaut""" db_config = config_collection.find_one({"_id": "system_config"}) if db_config: # Mettre à jour les variables de configuration config.MAX_IMAGE_SIZE = db_config["config"]["max_image_size"] config.MAX_IMAGES_PER_PIN = db_config["config"]["max_images_per_pin"] config.MAX_IMAGES_PER_USER = db_config["config"]["max_images_per_user"] config.ALLOWED_MIME_TYPES = db_config["config"]["allowed_image_types"] config.MAX_PINS_PER_USER = db_config["config"]["max_pins_per_user"] config.MAX_FRIENDS_PER_USER = db_config["config"]["max_friends_per_user"] else: # Créer la configuration par défaut dans la base de données default_config = DBConfig( config=SystemConfig(**config.DEFAULT_CONFIG), updated_at=datetime.now(), updated_by="system" ) config_collection.insert_one({ "_id": "system_config", **default_config.model_dump() }) # Charger la configuration au démarrage load_config() @admin_router.get( path="/stats", responses={401: {"model": HTTPError}, 403: {"model": HTTPError}} ) async def get_stats(admin_user: User = Depends(get_admin_user)): # Statistiques générales total_users = users_collection.count_documents({}) total_pins = pins_collection.count_documents({}) total_images = images_collection.count_documents({}) total_friends = friends_collection.count_documents({"status": "accepted"}) # Statistiques des 30 derniers jours thirty_days_ago = datetime.now() - timedelta(days=30) new_users = users_collection.count_documents({ "created_at": {"$gte": thirty_days_ago} }) new_pins = pins_collection.count_documents({ "created_at": {"$gte": thirty_days_ago} }) new_images = images_collection.count_documents({ "metadata.created_at": {"$gte": thirty_days_ago.isoformat()} }) # Top utilisateurs top_users = list(users_collection.aggregate([ {"$lookup": { "from": "pins", "let": { "user_id": { "$toString": "$_id" } }, "pipeline": [ { "$match": { "$expr": { "$eq": ["$user_id", "$$user_id"] } } } ], "as": "pins" }}, {"$project": { "username": 1, "pin_count": {"$size": "$pins"} }}, {"$sort": {"pin_count": -1}}, {"$limit": 5} ])) # Top pins (les plus partagés) top_pins = list(pin_permissions_collection.aggregate([ {"$group": { "_id": "$pin_id", "share_count": {"$sum": 1} }}, {"$sort": {"share_count": -1}}, {"$limit": 5}, {"$lookup": { "from": "pins", "localField": "_id", "foreignField": "_id", "as": "pin_info" }}, {"$unwind": "$pin_info"}, {"$project": { "pin_id": "$_id", "share_count": 1, "title": "$pin_info.title" }} ])) # Statistiques de stockage total_storage = sum( image["metadata"]["size"] for image in images_collection.find({}, {"metadata.size": 1}) ) return { "general": { "total_users": total_users, "total_pins": total_pins, "total_images": total_images, "total_friends": total_friends, "total_storage_bytes": total_storage }, "last_30_days": { "new_users": new_users, "new_pins": new_pins, "new_images": new_images }, "top_users": [serialize_mongo_doc(user) for user in top_users], "top_shared_pins": [serialize_mongo_doc(pin) for pin in top_pins] } @admin_router.get( path="/config", response_model=SystemConfig, responses={401: {"model": HTTPError}, 403: {"model": HTTPError}} ) async def get_config(admin_user: User = Depends(get_admin_user)): db_config = config_collection.find_one({"_id": "system_config"}) if not db_config: return SystemConfig(**config.DEFAULT_CONFIG) return SystemConfig(**db_config["config"]) @admin_router.patch( path="/config", response_model=SystemConfig, responses={401: {"model": HTTPError}, 403: {"model": HTTPError}} ) async def update_config( new_config: SystemConfig, admin_user: User = Depends(get_admin_user) ): # Mettre à jour la configuration en base de données db_config = DBConfig( config=new_config, updated_at=datetime.now(), updated_by=admin_user.uid ) config_collection.update_one( {"_id": "system_config"}, {"$set": db_config.model_dump()}, upsert=True ) # Mettre à jour les variables de configuration config.MAX_IMAGE_SIZE = new_config.max_image_size config.MAX_IMAGES_PER_PIN = new_config.max_images_per_pin config.MAX_IMAGES_PER_USER = new_config.max_images_per_user config.ALLOWED_MIME_TYPES = new_config.allowed_image_types config.MAX_PINS_PER_USER = new_config.max_pins_per_user config.MAX_FRIENDS_PER_USER = new_config.max_friends_per_user return new_config @admin_router.get( path="/users", responses={401: {"model": HTTPError}, 403: {"model": HTTPError}}, response_model=list[UserAdminDTO] ) async def list_users(admin_user: User = Depends(get_admin_user)): """Liste tous les utilisateurs (sans le mot de passe) - Route admin uniquement""" users = users_collection.find({}, {"password": 0}) # Exclure le mot de passe users_list = [] for user in users: user["uid"] = str(user["_id"]) user = UserAdminDTO(**user) users_list.append(user) return users_list @admin_router.delete( path="/user/{uid}", responses={401: {"model": HTTPError}, 403: {"model": HTTPError}, 404: {"model": HTTPError}, 400: {"model": HTTPError}} ) async def delete_user(uid: str, admin_user: User = Depends(get_admin_user)): try: ObjectId(uid) except: raise HTTPException(status_code=400, detail="UID invalide") user = users_collection.find_one({"_id": ObjectId(uid)}) if not user: raise HTTPException(status_code=404, detail="Utilisateur non trouvé") users_collection.delete_one({"_id": ObjectId(uid)}) return {"message": "Utilisateur supprimé avec succès"}