working version

This commit is contained in:
root
2025-08-27 18:34:38 +02:00
parent b7a84a53aa
commit dfaae262c7
153 changed files with 19389 additions and 788 deletions

View File

@@ -1,29 +0,0 @@
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
libpq-dev \
libmagic1 \
libgl1 \
libglib2.0-0 \
libsm6 \
libxext6 \
libxrender1 \
libgomp1 \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements first for better caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Create uploads directory
RUN mkdir -p /app/uploads
# Run the application
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

61
backend/Dockerfile.dev Executable file
View File

@@ -0,0 +1,61 @@
FROM python:3.11-slim
LABEL maintainer="LeDiscord Team"
LABEL version="1.0"
LABEL description="LeDiscord Backend - Environnement Développement"
WORKDIR /app
# Env dev
ENV ENVIRONMENT=development \
PYTHONPATH=/app \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
# Dépendances système (minifiées)
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
libmagic1 \
libgl1 \
libglib2.0-0 \
libsm6 \
libxext6 \
libxrender1 \
libgomp1 \
curl \
&& rm -rf /var/lib/apt/lists/*
# Requirements (cache friendly)
COPY requirements.txt .
# Uvicorn[standard] apporte watchfiles pour un --reload rapide/stable en dev
RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir -r requirements.txt \
&& pip install --no-cache-dir "uvicorn[standard]"
# Dossiers utiles
RUN mkdir -p /app/uploads /app/logs
# Code source
COPY . .
# Env dev
COPY .env.development .env
# Permissions
RUN chmod -R 755 /app
EXPOSE 8000
# Hot-reload + respect des en-têtes proxy (utile si tu testes derrière Traefik en dev)
# Astuce: on exclut uploads/logs du reload pour éviter les restarts inutiles
CMD ["uvicorn", "app:app", \
"--reload", \
"--reload-exclude", "uploads/*", \
"--reload-exclude", "logs/*", \
"--host", "0.0.0.0", \
"--port", "8000", \
"--log-level", "debug", \
"--proxy-headers", \
"--forwarded-allow-ips=*"]

75
backend/Dockerfile.prod Executable file
View File

@@ -0,0 +1,75 @@
# Multi-stage build pour la production
FROM python:3.11-slim AS builder
LABEL maintainer="LeDiscord Team"
LABEL version="1.0"
LABEL description="LeDiscord Backend - Environnement Production"
WORKDIR /app
# Env
ENV ENVIRONMENT=production \
PYTHONPATH=/app \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
# Dépendances de build
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Requirements
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir -r requirements.txt
# ---- Stage runtime ----
FROM python:3.11-slim AS production
WORKDIR /app
# Env runtime
ENV ENVIRONMENT=production \
PYTHONPATH=/app \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
# Dépendances runtime
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
libmagic1 \
libgl1 \
libglib2.0-0 \
libsm6 \
libxext6 \
libxrender1 \
libgomp1 \
&& rm -rf /var/lib/apt/lists/*
# Paquets Python depuis le builder
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
# Dossiers appli
RUN mkdir -p /app/uploads /app/logs
# Code source
COPY . .
# Env prod (attention à ce que tu y mets)
COPY .env.production .env
# Utilisateur non-root
RUN groupadd -r lediscord && useradd -r -g lediscord lediscord \
&& chown -R lediscord:lediscord /app
USER lediscord
EXPOSE 8000
# Healthcheck (optionnel mais pratique)
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD python -c "import socket; s=socket.socket(); s.settimeout(2); s.connect(('127.0.0.1',8000)); s.close()" || exit 1
# Démarrage uvicorn — pas de guillemets imbriqués ici
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4", "--log-level", "info", "--proxy-headers", "--forwarded-allow-ips=*"]

0
backend/__init__.py Normal file → Executable file
View File

0
backend/api/routers/__init__.py Normal file → Executable file
View File

0
backend/api/routers/admin.py Normal file → Executable file
View File

0
backend/api/routers/albums.py Normal file → Executable file
View File

0
backend/api/routers/auth.py Normal file → Executable file
View File

14
backend/api/routers/events.py Normal file → Executable file
View File

@@ -1,5 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from sqlalchemy.orm import Session, joinedload
from typing import List
from datetime import datetime
from config.database import get_db
@@ -64,7 +64,7 @@ async def get_events(
upcoming: bool = None
):
"""Get all events, optionally filtered by upcoming status."""
query = db.query(Event)
query = db.query(Event).options(joinedload(Event.creator))
if upcoming is True:
# Only upcoming events
@@ -83,7 +83,7 @@ async def get_upcoming_events(
current_user: User = Depends(get_current_active_user)
):
"""Get only upcoming events."""
events = db.query(Event).filter(
events = db.query(Event).options(joinedload(Event.creator)).filter(
Event.date >= datetime.utcnow()
).order_by(Event.date).all()
return [format_event_response(event, db) for event in events]
@@ -94,7 +94,7 @@ async def get_past_events(
current_user: User = Depends(get_current_active_user)
):
"""Get only past events."""
events = db.query(Event).filter(
events = db.query(Event).options(joinedload(Event.creator)).filter(
Event.date < datetime.utcnow()
).order_by(Event.date.desc()).all()
return [format_event_response(event, db) for event in events]
@@ -204,6 +204,9 @@ def format_event_response(event: Event, db: Session) -> dict:
participations = []
present_count = absent_count = maybe_count = pending_count = 0
# Get creator user directly
creator = db.query(User).filter(User.id == event.creator_id).first()
for p in event.participations:
user = db.query(User).filter(User.id == p.user_id).first()
participations.append({
@@ -235,7 +238,8 @@ def format_event_response(event: Event, db: Session) -> dict:
"cover_image": event.cover_image,
"created_at": event.created_at,
"updated_at": event.updated_at,
"creator_name": event.creator.full_name,
"creator_name": creator.full_name if creator else "Unknown",
"creator_avatar": creator.avatar_url if creator else None,
"participations": participations,
"present_count": present_count,
"absent_count": absent_count,

0
backend/api/routers/information.py Normal file → Executable file
View File

0
backend/api/routers/notifications.py Normal file → Executable file
View File

0
backend/api/routers/posts.py Normal file → Executable file
View File

24
backend/api/routers/settings.py Normal file → Executable file
View File

@@ -40,7 +40,7 @@ async def get_upload_limits(
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user)
):
"""Get current upload limits configuration."""
"""Get current upload limits configuration (admin only)."""
settings = db.query(SystemSettings).filter(
SystemSettings.category == "uploads"
).all()
@@ -61,6 +61,28 @@ async def get_upload_limits(
allowed_video_types=settings_dict.get("allowed_video_types", "video/mp4,video/mpeg,video/quicktime,video/webm").split(",")
)
@router.get("/public/upload-limits", response_model=UploadLimitsResponse)
async def get_public_upload_limits(
db: Session = Depends(get_db)
):
"""Get current upload limits configuration (public endpoint - no auth required)."""
settings = db.query(SystemSettings).filter(
SystemSettings.category == "uploads"
).all()
# Convertir en dictionnaire pour faciliter l'accès
settings_dict = {s.key: s.value for s in settings}
return UploadLimitsResponse(
max_album_size_mb=int(settings_dict.get("max_album_size_mb", "100")),
max_vlog_size_mb=int(settings_dict.get("max_vlog_size_mb", "500")),
max_image_size_mb=int(settings_dict.get("max_image_size_mb", "10")),
max_video_size_mb=int(settings_dict.get("max_video_size_mb", "100")),
max_media_per_album=int(settings_dict.get("max_media_per_album", "50")),
allowed_image_types=settings_dict.get("allowed_image_types", "image/jpeg,image/png,image/gif,image/webp").split(","),
allowed_video_types=settings_dict.get("allowed_video_types", "video/mp4,video/mpeg,video/quicktime,video/webm").split(",")
)
@router.post("/", response_model=SystemSettingResponse)
async def create_setting(
setting_data: SystemSettingCreate,

0
backend/api/routers/stats.py Normal file → Executable file
View File

0
backend/api/routers/tickets.py Normal file → Executable file
View File

0
backend/api/routers/users.py Normal file → Executable file
View File

0
backend/api/routers/vlogs.py Normal file → Executable file
View File

2
backend/app.py Normal file → Executable file
View File

@@ -194,7 +194,7 @@ app = FastAPI(
# Configure CORS
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_origins=settings.CORS_ORIGINS_LIST,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],

0
backend/config/database.py Normal file → Executable file
View File

127
backend/config/settings.py Normal file → Executable file
View File

@@ -1,37 +1,118 @@
from typing import List
import os
from pathlib import Path
class Settings:
# Database
DATABASE_URL: str = os.getenv("DATABASE_URL", "postgresql://lediscord_user:lediscord_password@postgres:5432/lediscord")
"""Configuration principale avec variables d'environnement obligatoires"""
# JWT
JWT_SECRET_KEY: str = "your-super-secret-jwt-key-change-me"
JWT_ALGORITHM: str = "HS256"
JWT_EXPIRATION_MINUTES: int = 10080 # 7 days
# Environnement - OBLIGATOIRE
ENVIRONMENT: str = os.getenv("ENVIRONMENT")
if not ENVIRONMENT:
raise ValueError("ENVIRONMENT variable is required. Use .env.local, .env.development, or .env.production")
# Debug et reload
DEBUG: bool = os.getenv("DEBUG", "false").lower() == "true"
RELOAD: bool = os.getenv("RELOAD", "false").lower() == "true"
# Database - OBLIGATOIRE
DATABASE_URL: str = os.getenv("DATABASE_URL")
if not DATABASE_URL:
raise ValueError("DATABASE_URL variable is required")
# JWT - OBLIGATOIRE
JWT_SECRET_KEY: str = os.getenv("JWT_SECRET_KEY")
if not JWT_SECRET_KEY:
raise ValueError("JWT_SECRET_KEY variable is required")
JWT_ALGORITHM: str = os.getenv("JWT_ALGORITHM", "HS256")
JWT_EXPIRATION_MINUTES: int = int(os.getenv("JWT_EXPIRATION_MINUTES", "10080"))
# Upload
UPLOAD_PATH: str = "/app/uploads"
MAX_UPLOAD_SIZE: int = 100 * 1024 * 1024 # 100MB
ALLOWED_IMAGE_TYPES: List[str] = ["image/jpeg", "image/png", "image/gif", "image/webp"]
ALLOWED_VIDEO_TYPES: List[str] = ["video/mp4", "video/mpeg", "video/quicktime", "video/webm"]
UPLOAD_PATH: str = os.getenv("UPLOAD_PATH", "./uploads")
MAX_UPLOAD_SIZE: int = int(os.getenv("MAX_UPLOAD_SIZE", "104857600"))
ALLOWED_IMAGE_TYPES: List[str] = os.getenv("ALLOWED_IMAGE_TYPES", "image/jpeg,image/png,image/gif,image/webp").split(",")
ALLOWED_VIDEO_TYPES: List[str] = os.getenv("ALLOWED_VIDEO_TYPES", "video/mp4,video/mpeg,video/quicktime,video/webm").split(",")
# CORS - Fixed list, no environment parsing
CORS_ORIGINS: List[str] = ["http://localhost:5173", "http://localhost:3000"]
# CORS - OBLIGATOIRE
CORS_ORIGINS: str = os.getenv("CORS_ORIGINS")
if not CORS_ORIGINS:
raise ValueError("CORS_ORIGINS variable is required")
CORS_ORIGINS_LIST: List[str] = CORS_ORIGINS.split(",")
# Email
SMTP_HOST: str = "smtp.gmail.com"
SMTP_PORT: int = 587
SMTP_USER: str = ""
SMTP_PASSWORD: str = ""
SMTP_FROM: str = "noreply@lediscord.com"
SMTP_HOST: str = os.getenv("SMTP_HOST", "smtp.gmail.com")
SMTP_PORT: int = int(os.getenv("SMTP_PORT", "587"))
SMTP_USER: str = os.getenv("SMTP_USER", "")
SMTP_PASSWORD: str = os.getenv("SMTP_PASSWORD", "")
SMTP_FROM: str = os.getenv("SMTP_FROM", "noreply@lediscord.com")
# Admin
ADMIN_EMAIL: str = "admin@lediscord.com"
ADMIN_PASSWORD: str = "admin123"
# Admin - OBLIGATOIRE
ADMIN_EMAIL: str = os.getenv("ADMIN_EMAIL")
if not ADMIN_EMAIL:
raise ValueError("ADMIN_EMAIL variable is required")
ADMIN_PASSWORD: str = os.getenv("ADMIN_PASSWORD")
if not ADMIN_PASSWORD:
raise ValueError("ADMIN_PASSWORD variable is required")
# App
APP_NAME: str = "LeDiscord"
APP_URL: str = "http://localhost:5173"
APP_NAME: str = os.getenv("APP_NAME", "LeDiscord")
APP_URL: str = os.getenv("APP_URL", "http://localhost:5173")
# Logging
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO" if ENVIRONMENT == "production" else "DEBUG")
# Performance
WORKERS: int = int(os.getenv("WORKERS", "1" if ENVIRONMENT in ["local", "development"] else "4"))
# Sécurité (production uniquement)
SECURE_COOKIES: bool = os.getenv("SECURE_COOKIES", "false").lower() == "true"
SECURE_HEADERS: bool = os.getenv("SECURE_HEADERS", "false").lower() == "true"
def __str__(self) -> str:
"""Représentation lisible de la configuration"""
return f"""
🔧 Configuration LeDiscord - Environnement: {self.ENVIRONMENT.upper()}
📊 Debug: {self.DEBUG}
🔄 Reload: {self.RELOAD}
🌐 CORS Origins: {', '.join(self.CORS_ORIGINS_LIST)}
🗄️ Database: {self.DATABASE_URL.split('@')[1] if '@' in self.DATABASE_URL else 'Unknown'}
📁 Upload Path: {self.UPLOAD_PATH}
📝 Log Level: {self.LOG_LEVEL}
🚀 Workers: {self.WORKERS}
"""
def validate(self) -> bool:
"""Valide la configuration"""
required_vars = [
"ENVIRONMENT",
"DATABASE_URL",
"JWT_SECRET_KEY",
"CORS_ORIGINS",
"ADMIN_EMAIL",
"ADMIN_PASSWORD"
]
for var in required_vars:
if not getattr(self, var):
print(f"❌ Configuration invalide: {var} est manquant")
return False
print("✅ Configuration validée avec succès")
return True
settings = Settings()
# Instance globale
try:
settings = Settings()
except ValueError as e:
print(f"❌ Erreur de configuration: {e}")
print("💡 Assurez-vous d'utiliser un fichier d'environnement spécifique:")
print(" - .env.local pour le développement local")
print(" - .env.development pour l'environnement de développement")
print(" - .env.production pour la production")
raise
# Validation automatique au démarrage
if __name__ == "__main__":
print(settings)
settings.validate()

0
backend/models/__init__.py Normal file → Executable file
View File

0
backend/models/album.py Normal file → Executable file
View File

0
backend/models/event.py Normal file → Executable file
View File

0
backend/models/information.py Normal file → Executable file
View File

0
backend/models/notification.py Normal file → Executable file
View File

0
backend/models/post.py Normal file → Executable file
View File

0
backend/models/settings.py Normal file → Executable file
View File

0
backend/models/ticket.py Normal file → Executable file
View File

0
backend/models/user.py Normal file → Executable file
View File

0
backend/models/vlog.py Normal file → Executable file
View File

0
backend/requirements.txt Normal file → Executable file
View File

0
backend/schemas/__init__.py Normal file → Executable file
View File

0
backend/schemas/album.py Normal file → Executable file
View File

1
backend/schemas/event.py Normal file → Executable file
View File

@@ -40,6 +40,7 @@ class EventResponse(EventBase):
id: int
creator_id: int
creator_name: str
creator_avatar: Optional[str] = None
cover_image: Optional[str]
created_at: datetime
participations: List[ParticipationResponse] = []

0
backend/schemas/information.py Normal file → Executable file
View File

0
backend/schemas/notification.py Normal file → Executable file
View File

0
backend/schemas/post.py Normal file → Executable file
View File

0
backend/schemas/settings.py Normal file → Executable file
View File

0
backend/schemas/ticket.py Normal file → Executable file
View File

0
backend/schemas/user.py Normal file → Executable file
View File

0
backend/schemas/vlog.py Normal file → Executable file
View File

84
backend/test_config.py Executable file
View File

@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""
Script de test pour vérifier la configuration des environnements
Usage: python test_config.py [local|development|production]
"""
import os
import sys
from pathlib import Path
def test_environment(env_name: str):
"""Teste la configuration d'un environnement spécifique"""
print(f"🔍 Test de la configuration {env_name.upper()}")
print("=" * 50)
# Définir l'environnement
os.environ["ENVIRONMENT"] = env_name
try:
# Importer la configuration
from config.settings import settings
print(f"✅ Configuration chargée avec succès")
print(f" Environnement détecté: {settings.ENVIRONMENT}")
print(f" Debug: {settings.DEBUG}")
print(f" Reload: {settings.RELOAD}")
print(f" Log Level: {settings.LOG_LEVEL}")
print(f" Workers: {settings.WORKERS}")
print(f" CORS Origins: {', '.join(settings.CORS_ORIGINS)}")
print(f" Upload Path: {settings.UPLOAD_PATH}")
print(f" Database: {settings.DATABASE_URL.split('@')[1] if '@' in settings.DATABASE_URL else 'Unknown'}")
# Validation
if settings.validate():
print(f"✅ Configuration {env_name} validée avec succès")
else:
print(f"❌ Configuration {env_name} invalide")
except Exception as e:
print(f"❌ Erreur lors du chargement de la configuration {env_name}: {e}")
return False
print()
return True
def main():
"""Fonction principale"""
print("🚀 Test de configuration LeDiscord Backend")
print("=" * 60)
# Vérifier que nous sommes dans le bon répertoire
if not Path("config/settings.py").exists():
print("❌ Erreur: Ce script doit être exécuté depuis le répertoire backend/")
sys.exit(1)
# Test des environnements
environments = ["local", "development", "production"]
if len(sys.argv) > 1:
env_arg = sys.argv[1].lower()
if env_arg in environments:
environments = [env_arg]
else:
print(f"❌ Environnement invalide: {env_arg}")
print(f" Environnements valides: {', '.join(environments)}")
sys.exit(1)
success_count = 0
for env in environments:
if test_environment(env):
success_count += 1
print("=" * 60)
print(f"📊 Résultats: {success_count}/{len(environments)} configurations valides")
if success_count == len(environments):
print("🎉 Toutes les configurations sont valides !")
return 0
else:
print("⚠️ Certaines configurations ont des problèmes")
return 1
if __name__ == "__main__":
sys.exit(main())

0
backend/utils/email.py Normal file → Executable file
View File

0
backend/utils/init_db.py Normal file → Executable file
View File

0
backend/utils/notification_service.py Normal file → Executable file
View File

0
backend/utils/security.py Normal file → Executable file
View File

0
backend/utils/settings_service.py Normal file → Executable file
View File

0
backend/utils/video_utils.py Normal file → Executable file
View File