You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
234 lines
7.8 KiB
234 lines
7.8 KiB
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"}
|