Files
LeDiscord/backend/app.py
EvanChal f33dfd5ab7
Some checks failed
Deploy to Development / build-and-deploy (push) Failing after 46s
Deploy to Production / build-and-deploy (push) Successful in 1m47s
fix(notification+vlog upload)
2026-01-27 02:39:51 +01:00

313 lines
12 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, push
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...")
# Note: Database migrations are handled by Alembic
# Run migrations manually with: alembic upgrade head
# Base.metadata.create_all(bind=engine) # Disabled in favor of Alembic migrations
# 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(push.router, prefix="/api/push", tags=["Push 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"}