311 lines
11 KiB
Python
311 lines
11 KiB
Python
from fastapi import FastAPI, Request, Response, HTTPException
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.staticfiles import StaticFiles
|
|
from contextlib import asynccontextmanager
|
|
import os
|
|
from pathlib import Path
|
|
import mimetypes
|
|
|
|
from config.settings import settings
|
|
from config.database import engine, Base
|
|
from api.routers import auth, users, events, albums, posts, vlogs, stats, admin, notifications, settings as settings_router, information, tickets
|
|
from utils.init_db import init_database
|
|
from utils.settings_service import SettingsService
|
|
from config.database import SessionLocal
|
|
|
|
# Create uploads directory if it doesn't exist
|
|
Path(settings.UPLOAD_PATH).mkdir(parents=True, exist_ok=True)
|
|
|
|
def init_default_settings():
|
|
"""Initialize default system settings."""
|
|
db = SessionLocal()
|
|
try:
|
|
# Check if settings already exist
|
|
from models.settings import SystemSettings
|
|
existing_settings = db.query(SystemSettings).count()
|
|
|
|
if existing_settings == 0:
|
|
print("Initializing default system settings...")
|
|
|
|
default_settings = [
|
|
{
|
|
"key": "max_album_size_mb",
|
|
"value": "100",
|
|
"description": "Taille maximale des albums en MB",
|
|
"category": "uploads"
|
|
},
|
|
{
|
|
"key": "max_vlog_size_mb",
|
|
"value": "500",
|
|
"description": "Taille maximale des vlogs en MB",
|
|
"category": "uploads"
|
|
},
|
|
{
|
|
"key": "max_image_size_mb",
|
|
"value": "10",
|
|
"description": "Taille maximale des images en MB",
|
|
"category": "uploads"
|
|
},
|
|
{
|
|
"key": "max_video_size_mb",
|
|
"value": "100",
|
|
"description": "Taille maximale des vidéos en MB",
|
|
"category": "uploads"
|
|
},
|
|
{
|
|
"key": "max_media_per_album",
|
|
"value": "50",
|
|
"description": "Nombre maximum de médias par album",
|
|
"category": "uploads"
|
|
},
|
|
{
|
|
"key": "allowed_image_types",
|
|
"value": "image/jpeg,image/png,image/gif,image/webp",
|
|
"description": "Types d'images autorisés (séparés par des virgules)",
|
|
"category": "uploads"
|
|
},
|
|
{
|
|
"key": "allowed_video_types",
|
|
"value": "video/mp4,video/mpeg,video/quicktime,video/webm",
|
|
"description": "Types de vidéos autorisés (séparés par des virgules)",
|
|
"category": "uploads"
|
|
},
|
|
{
|
|
"key": "max_users",
|
|
"value": "50",
|
|
"description": "Nombre maximum d'utilisateurs",
|
|
"category": "general"
|
|
},
|
|
{
|
|
"key": "enable_registration",
|
|
"value": "true",
|
|
"description": "Autoriser les nouvelles inscriptions",
|
|
"category": "general"
|
|
}
|
|
]
|
|
|
|
for setting_data in default_settings:
|
|
setting = SystemSettings(**setting_data)
|
|
db.add(setting)
|
|
|
|
db.commit()
|
|
print(f"Created {len(default_settings)} default settings")
|
|
else:
|
|
print("System settings already exist, checking for missing settings...")
|
|
|
|
# Check for missing settings and add them
|
|
all_settings = [
|
|
{
|
|
"key": "max_album_size_mb",
|
|
"value": "100",
|
|
"description": "Taille maximale des albums en MB",
|
|
"category": "uploads"
|
|
},
|
|
{
|
|
"key": "max_vlog_size_mb",
|
|
"value": "500",
|
|
"description": "Taille maximale des vlogs en MB",
|
|
"category": "uploads"
|
|
},
|
|
{
|
|
"key": "max_image_size_mb",
|
|
"value": "10",
|
|
"description": "Taille maximale des images en MB",
|
|
"category": "uploads"
|
|
},
|
|
{
|
|
"key": "max_video_size_mb",
|
|
"value": "100",
|
|
"description": "Taille maximale des vidéos en MB",
|
|
"category": "uploads"
|
|
},
|
|
{
|
|
"key": "max_media_per_album",
|
|
"value": "50",
|
|
"description": "Nombre maximum de médias par album",
|
|
"category": "uploads"
|
|
},
|
|
{
|
|
"key": "allowed_image_types",
|
|
"value": "image/jpeg,image/png,image/gif,image/webp",
|
|
"description": "Types d'images autorisés (séparés par des virgules)",
|
|
"category": "uploads"
|
|
},
|
|
{
|
|
"key": "allowed_video_types",
|
|
"value": "video/mp4,video/mpeg,video/quicktime,video/webm",
|
|
"description": "Types de vidéos autorisés (séparés par des virgules)",
|
|
"category": "uploads"
|
|
},
|
|
{
|
|
"key": "max_users",
|
|
"value": "50",
|
|
"description": "Nombre maximum d'utilisateurs",
|
|
"category": "general"
|
|
},
|
|
{
|
|
"key": "enable_registration",
|
|
"value": "true",
|
|
"description": "Autoriser les nouvelles inscriptions",
|
|
"category": "general"
|
|
}
|
|
]
|
|
|
|
added_count = 0
|
|
for setting_data in all_settings:
|
|
existing = db.query(SystemSettings).filter(SystemSettings.key == setting_data["key"]).first()
|
|
if not existing:
|
|
setting = SystemSettings(**setting_data)
|
|
db.add(setting)
|
|
added_count += 1
|
|
|
|
if added_count > 0:
|
|
db.commit()
|
|
print(f"Added {added_count} missing settings")
|
|
else:
|
|
print("All settings are already present")
|
|
|
|
except Exception as e:
|
|
print(f"Error initializing settings: {e}")
|
|
db.rollback()
|
|
finally:
|
|
db.close()
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
# Startup
|
|
print("Starting LeDiscord backend...")
|
|
# Create tables
|
|
Base.metadata.create_all(bind=engine)
|
|
# Initialize database with admin user
|
|
init_database()
|
|
# Initialize default settings
|
|
init_default_settings()
|
|
yield
|
|
# Shutdown
|
|
print("Shutting down LeDiscord backend...")
|
|
|
|
app = FastAPI(
|
|
title="LeDiscord API",
|
|
description="API pour la plateforme communautaire LeDiscord",
|
|
version="1.0.0",
|
|
lifespan=lifespan
|
|
)
|
|
|
|
# Configure CORS
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=settings.CORS_ORIGINS_LIST,
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
expose_headers=["Content-Range", "Accept-Ranges"],
|
|
)
|
|
|
|
# Endpoint personnalisé pour servir les vidéos avec support Range
|
|
@app.get("/uploads/{file_path:path}")
|
|
async def serve_media_with_range(request: Request, file_path: str):
|
|
"""
|
|
Serve media files with proper Range request support for video scrubbing.
|
|
"""
|
|
file_full_path = Path(settings.UPLOAD_PATH) / file_path
|
|
|
|
# Vérifier que le fichier existe
|
|
if not file_full_path.exists() or not file_full_path.is_file():
|
|
raise HTTPException(status_code=404, detail="File not found")
|
|
|
|
# Vérifier que le fichier est dans le répertoire uploads (sécurité)
|
|
try:
|
|
file_full_path.resolve().relative_to(Path(settings.UPLOAD_PATH).resolve())
|
|
except ValueError:
|
|
raise HTTPException(status_code=403, detail="Access denied")
|
|
|
|
# Obtenir la taille du fichier
|
|
file_size = file_full_path.stat().st_size
|
|
|
|
# Déterminer le content type
|
|
content_type, _ = mimetypes.guess_type(str(file_full_path))
|
|
if not content_type:
|
|
content_type = "application/octet-stream"
|
|
|
|
# Gérer les requêtes Range
|
|
range_header = request.headers.get("Range")
|
|
|
|
if range_header:
|
|
# Parser le header Range (format: bytes=start-end)
|
|
range_match = range_header.replace("bytes=", "").split("-")
|
|
start = int(range_match[0]) if range_match[0] else 0
|
|
end = int(range_match[1]) if range_match[1] and range_match[1] else file_size - 1
|
|
|
|
# Valider la plage
|
|
if start >= file_size or end >= file_size or start > end:
|
|
return Response(
|
|
status_code=416,
|
|
headers={
|
|
"Content-Range": f"bytes */{file_size}",
|
|
"Accept-Ranges": "bytes"
|
|
}
|
|
)
|
|
|
|
# Lire la plage demandée
|
|
chunk_size = end - start + 1
|
|
with open(file_full_path, "rb") as f:
|
|
f.seek(start)
|
|
chunk = f.read(chunk_size)
|
|
|
|
# Retourner la réponse 206 Partial Content
|
|
return Response(
|
|
content=chunk,
|
|
status_code=206,
|
|
headers={
|
|
"Content-Range": f"bytes {start}-{end}/{file_size}",
|
|
"Accept-Ranges": "bytes",
|
|
"Content-Length": str(chunk_size),
|
|
"Content-Type": content_type,
|
|
},
|
|
media_type=content_type
|
|
)
|
|
else:
|
|
# Pas de Range header, retourner le fichier complet
|
|
with open(file_full_path, "rb") as f:
|
|
content = f.read()
|
|
|
|
return Response(
|
|
content=content,
|
|
headers={
|
|
"Accept-Ranges": "bytes",
|
|
"Content-Length": str(file_size),
|
|
"Content-Type": content_type,
|
|
},
|
|
media_type=content_type
|
|
)
|
|
|
|
# Note: StaticFiles mount retiré car notre endpoint personnalisé gère tous les fichiers
|
|
# avec support Range pour permettre le scrubbing vidéo
|
|
|
|
# Include routers
|
|
app.include_router(auth.router, prefix="/api/auth", tags=["Authentication"])
|
|
app.include_router(users.router, prefix="/api/users", tags=["Users"])
|
|
app.include_router(events.router, prefix="/api/events", tags=["Events"])
|
|
app.include_router(albums.router, prefix="/api/albums", tags=["Albums"])
|
|
app.include_router(posts.router, prefix="/api/posts", tags=["Posts"])
|
|
app.include_router(vlogs.router, prefix="/api/vlogs", tags=["Vlogs"])
|
|
app.include_router(stats.router, prefix="/api/stats", tags=["Statistics"])
|
|
app.include_router(admin.router, prefix="/api/admin", tags=["Admin"])
|
|
app.include_router(notifications.router, prefix="/api/notifications", tags=["Notifications"])
|
|
app.include_router(settings_router.router, prefix="/api/settings", tags=["Settings"])
|
|
app.include_router(information.router, prefix="/api/information", tags=["Information"])
|
|
app.include_router(tickets.router, prefix="/api/tickets", tags=["Tickets"])
|
|
|
|
@app.get("/")
|
|
async def root():
|
|
return {
|
|
"message": "Bienvenue sur LeDiscord API",
|
|
"version": "1.0.0",
|
|
"docs": "/docs"
|
|
}
|
|
|
|
@app.get("/health")
|
|
async def health_check():
|
|
return {"status": "healthy"}
|