diff --git a/api.py b/api.py index 462d6aa..f01767f 100644 --- a/api.py +++ b/api.py @@ -297,11 +297,11 @@ async def lifespan(app: FastAPI): await init_db() logger.info("✅ Base de données initialisée") - # Injecter session_factory dans email_queue + # ✅ CORRECTION: Injecter session_factory ET sage_client dans email_queue email_queue.session_factory = async_session_factory - - # ⚠️ PAS de sage_connector ici (c'est sur Windows !) - # email_queue utilisera sage_client pour générer les PDFs via HTTP + email_queue.sage_client = sage_client + + logger.info("✅ sage_client injecté dans email_queue") # Démarrer queue email_queue.start(num_workers=settings.max_email_workers) @@ -1739,6 +1739,212 @@ async def lire_utilisateur_debug( raise HTTPException(500, str(e)) +# À ajouter dans api.py dans la section Debug + +@app.get("/debug/database/check", tags=["Debug"]) +async def verifier_integrite_database(session: AsyncSession = Depends(get_session)): + """ + 🔍 Vérification de l'intégrité de la base de données + + Retourne des statistiques détaillées sur toutes les tables + """ + from database import User, RefreshToken, LoginAttempt, EmailLog, SignatureLog + from sqlalchemy import func, text + + try: + diagnostics = {} + + # === TABLE USERS === + # Compter tous les users + total_users = await session.execute(select(func.count(User.id))) + diagnostics["users"] = { + "total": total_users.scalar(), + "details": [] + } + + # Lister tous les users avec détails + all_users = await session.execute(select(User)) + users_list = all_users.scalars().all() + + for u in users_list: + diagnostics["users"]["details"].append({ + "id": u.id, + "email": u.email, + "nom": f"{u.prenom} {u.nom}", + "role": u.role, + "is_active": u.is_active, + "is_verified": u.is_verified, + "created_at": u.created_at.isoformat() if u.created_at else None, + "has_reset_token": u.reset_token is not None, + "has_verification_token": u.verification_token is not None, + }) + + # === TABLE REFRESH_TOKENS === + total_tokens = await session.execute(select(func.count(RefreshToken.id))) + diagnostics["refresh_tokens"] = { + "total": total_tokens.scalar() + } + + # === TABLE LOGIN_ATTEMPTS === + total_attempts = await session.execute(select(func.count(LoginAttempt.id))) + diagnostics["login_attempts"] = { + "total": total_attempts.scalar() + } + + # === TABLE EMAIL_LOGS === + total_emails = await session.execute(select(func.count(EmailLog.id))) + diagnostics["email_logs"] = { + "total": total_emails.scalar() + } + + # === TABLE SIGNATURE_LOGS === + total_signatures = await session.execute(select(func.count(SignatureLog.id))) + diagnostics["signature_logs"] = { + "total": total_signatures.scalar() + } + + # === VÉRIFIER LES FICHIERS SQLITE === + import os + db_file = "sage_dataven.db" + diagnostics["database_file"] = { + "exists": os.path.exists(db_file), + "size_bytes": os.path.getsize(db_file) if os.path.exists(db_file) else 0, + "path": os.path.abspath(db_file) + } + + # === TESTER UNE REQUÊTE RAW SQL === + try: + raw_count = await session.execute(text("SELECT COUNT(*) FROM users")) + diagnostics["raw_sql_check"] = { + "users_count": raw_count.scalar(), + "status": "✅ Connexion DB OK" + } + except Exception as e: + diagnostics["raw_sql_check"] = { + "status": "❌ Erreur", + "error": str(e) + } + + return { + "success": True, + "timestamp": datetime.now().isoformat(), + "diagnostics": diagnostics + } + + except Exception as e: + logger.error(f"❌ Erreur diagnostic DB: {e}", exc_info=True) + raise HTTPException(500, f"Erreur diagnostic: {str(e)}") + + +@app.post("/debug/database/test-user-persistence", tags=["Debug"]) +async def tester_persistance_utilisateur(session: AsyncSession = Depends(get_session)): + """ + 🧪 Test de création/lecture/modification d'un utilisateur de test + + Crée un utilisateur de test, le modifie, et vérifie la persistance + """ + import uuid + from database import User + from security.auth import hash_password + + try: + test_email = f"test_{uuid.uuid4().hex[:8]}@example.com" + + # === ÉTAPE 1: CRÉATION === + test_user = User( + id=str(uuid.uuid4()), + email=test_email, + hashed_password=hash_password("TestPassword123!"), + nom="Test", + prenom="User", + role="user", + is_verified=True, + is_active=True, + created_at=datetime.now() + ) + + session.add(test_user) + await session.flush() + user_id = test_user.id + await session.commit() + + logger.info(f"✅ ÉTAPE 1: User créé - {user_id}") + + # === ÉTAPE 2: LECTURE === + result = await session.execute( + select(User).where(User.id == user_id) + ) + loaded_user = result.scalar_one_or_none() + + if not loaded_user: + return { + "success": False, + "error": "❌ User introuvable après création !", + "step": "LECTURE" + } + + logger.info(f"✅ ÉTAPE 2: User chargé - {loaded_user.email}") + + # === ÉTAPE 3: MODIFICATION (simulate reset password) === + loaded_user.hashed_password = hash_password("NewPassword456!") + loaded_user.reset_token = None + loaded_user.reset_token_expires = None + + session.add(loaded_user) + await session.flush() + await session.commit() + await session.refresh(loaded_user) + + logger.info(f"✅ ÉTAPE 3: User modifié") + + # === ÉTAPE 4: RE-LECTURE === + result2 = await session.execute( + select(User).where(User.id == user_id) + ) + reloaded_user = result2.scalar_one_or_none() + + if not reloaded_user: + return { + "success": False, + "error": "❌ User DISPARU après modification !", + "step": "RE-LECTURE", + "user_id": user_id + } + + logger.info(f"✅ ÉTAPE 4: User re-chargé - {reloaded_user.email}") + + # === ÉTAPE 5: SUPPRESSION DU TEST === + await session.delete(reloaded_user) + await session.commit() + + logger.info(f"✅ ÉTAPE 5: User test supprimé") + + return { + "success": True, + "message": "✅ Tous les tests de persistance sont OK", + "test_user_id": user_id, + "test_email": test_email, + "steps_completed": [ + "1. Création", + "2. Lecture", + "3. Modification (reset password simulé)", + "4. Re-lecture (vérification persistance)", + "5. Suppression (cleanup)" + ] + } + + except Exception as e: + logger.error(f"❌ Erreur test persistance: {e}", exc_info=True) + + # Rollback en cas d'erreur + await session.rollback() + + return { + "success": False, + "error": str(e), + "traceback": str(e.__class__.__name__) + } + # ===================================================== # LANCEMENT # ===================================================== diff --git a/core/dependencies.py b/core/dependencies.py index a860f5c..48bb868 100644 --- a/core/dependencies.py +++ b/core/dependencies.py @@ -5,6 +5,7 @@ from sqlalchemy import select from database import get_session, User from security.auth import decode_token from typing import Optional +from datetime import datetime # ✅ AJOUT MANQUANT - C'ÉTAIT LE BUG ! security = HTTPBearer() @@ -75,7 +76,7 @@ async def get_current_user( detail="Email non vérifié. Consultez votre boîte de réception." ) - # Vérifier si le compte est verrouillé + # ✅ FIX: Vérifier si le compte est verrouillé (maintenant datetime est importé!) if user.locked_until and user.locked_until > datetime.now(): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, diff --git a/email_queue.py b/email_queue.py index 9c8451d..beda62e 100644 --- a/email_queue.py +++ b/email_queue.py @@ -31,7 +31,7 @@ class EmailQueue: self.workers = [] self.running = False self.session_factory = None - self.sage_client = None # Sera injecté depuis api.py + self.sage_client = None def start(self, num_workers: int = 3): """Démarre les workers"""