""" Authentication API Endpoints. This module provides endpoints for user authentication including login, registration, and logout. """ from datetime import datetime from typing import Optional from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from passlib.context import CryptContext from app.database import get_db from app.models.user import User from app.models.badge import Badge, UserBadge from app.schemas.auth import ( LoginRequest, RegisterRequest, AuthResponse, ErrorResponse ) # #region agent log def write_debug_log(hypothesisId: str, location: str, message: str, data: dict = None): """Écrit un log NDJSON pour le debug.""" import json log_entry = { "sessionId": "debug-session", "runId": "run1", "hypothesisId": hypothesisId, "location": location, "message": message, "data": data or {}, "timestamp": datetime.now().timestamp() * 1000 } with open(r"d:\\dev_new_pc\\chartbastan\\.cursor\\debug.log", "a") as f: f.write(json.dumps(log_entry) + "\n") # #endregion # Configuration du hashage de mot de passe pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") router = APIRouter(tags=["authentication"]) # #region agent log write_debug_log("C", "auth.py:29", "Auth router initialized", {"prefix": "/api/v1/auth", "tags": ["authentication"]}) # #endregion def verify_password(plain_password: str, hashed_password: str) -> bool: """Vérifie un mot de passe en clair contre le hash""" return pwd_context.verify(plain_password, hashed_password) def get_password_hash(password: str) -> str: """Génère un hash sécurisé pour un mot de passe""" return pwd_context.hash(password) def generate_referral_code() -> str: """Génère un code de parrainage unique de 8 caractères""" import secrets import string alphabet = string.ascii_uppercase + string.digits return ''.join(secrets.choice(alphabet) for _ in range(8)) @router.post("/login", response_model=AuthResponse, responses={401: {"model": ErrorResponse}}) def login_user( request: LoginRequest, db: Session = Depends(get_db) ): """ Connecter un utilisateur avec email et mot de passe. Args: request: Email et mot de passe de l'utilisateur db: Session de base de données Returns: AuthResponse avec token et données utilisateur Raises: 401: Si email ou mot de passe incorrect 500: Si erreur serveur """ # #region agent log write_debug_log("D", "auth.py:48", "Login endpoint called", {"email": request.email}) # #endregion user = db.query(User).filter(User.email == request.email).first() # #region agent log write_debug_log("D", "auth.py:67", "User lookup", {"email": request.email, "user_found": user is not None}) # #endregion # Vérifier si l'utilisateur existe if not user: # #region agent log write_debug_log("D", "auth.py:70", "User not found", {"email": request.email}) # #endregion raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Email ou mot de passe incorrect" ) # Vérifier le mot de passe if not user.password_hash: # #region agent log write_debug_log("D", "auth.py:81", "No password hash", {"email": request.email, "user_id": user.id}) # #endregion raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Email ou mot de passe incorrect" ) password_valid = verify_password(request.password, user.password_hash) # #region agent log write_debug_log("D", "auth.py:91", "Password verification", {"email": request.email, "user_id": user.id, "password_valid": password_valid}) # #endregion if not password_valid: # #region agent log write_debug_log("D", "auth.py:96", "Invalid password", {"email": request.email, "user_id": user.id}) # #endregion raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Email ou mot de passe incorrect" ) # Retourner les données utilisateur (sans le hash de mot de passe) # #region agent log write_debug_log("D", "auth.py:107", "Login successful", {"email": request.email, "user_id": user.id}) # #endregion return { "data": { "id": user.id, "email": user.email, "name": user.name, "is_premium": user.is_premium, "referral_code": user.referral_code, "created_at": user.created_at, }, "meta": { "timestamp": datetime.utcnow().isoformat(), "version": "v1" } } @router.post("/register", response_model=AuthResponse, status_code=status.HTTP_201_CREATED) def register_user( request: RegisterRequest, db: Session = Depends(get_db) ): """ Inscrire un nouvel utilisateur. Args: request: Email, mot de passe, nom optionnel et code de parrainage db: Session de base de données Returns: AuthResponse avec données utilisateur Raises: 400: Si validation échoue 409: Si email déjà utilisé 500: Si erreur serveur """ # Vérifier si l'email existe déjà existing_user = db.query(User).filter(User.email == request.email).first() if existing_user: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Cet email est déjà utilisé" ) # Valider le code de parrainage si fourni referrer: Optional[User] = None if request.referral_code: referrer = db.query(User).filter( User.referral_code == request.referral_code ).first() if not referrer: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Code de parrainage invalide" ) # Générer un code de parrainage unique referral_code = generate_referral_code() while db.query(User).filter(User.referral_code == referral_code).first(): referral_code = generate_referral_code() # Hasher le mot de passe password_hash = get_password_hash(request.password) # Créer l'utilisateur new_user = User( email=request.email, password_hash=password_hash, name=request.name, is_premium=False, referral_code=referral_code, created_at=datetime.utcnow(), updated_at=datetime.utcnow() ) db.add(new_user) db.commit() db.refresh(new_user) # Attribuer le badge de bienvenue (première connexion) welcome_badge = db.query(Badge).filter( Badge.badge_id == "first_login" ).first() if welcome_badge: user_badge = UserBadge( user_id=new_user.id, badge_id=welcome_badge.id, unlocked_at=datetime.utcnow() ) db.add(user_badge) db.commit() return { "data": { "id": new_user.id, "email": new_user.email, "name": new_user.name, "is_premium": new_user.is_premium, "referral_code": new_user.referral_code, "created_at": new_user.created_at, }, "meta": { "timestamp": datetime.utcnow().isoformat(), "version": "v1" } } @router.post("/logout") def logout_user(): """ Déconnecter l'utilisateur. Note: Le backend utilise des cookies côté client. Cette endpoint est disponible pour compatibilité future avec JWT. """ return { "data": { "message": "Déconnexion réussie" }, "meta": { "timestamp": datetime.utcnow().isoformat(), "version": "v1" } }