refactor(auth): reorganize imports and remove unused dependencies - Added some missing auth

This commit is contained in:
Fanilo-Nantenaina 2026-01-16 13:22:13 +03:00
parent 18d72b3bf9
commit 18603ded6e
4 changed files with 35 additions and 432 deletions

293
api.py
View file

@ -16,9 +16,10 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
import os
from pathlib import Path as FilePath
from data.data import TAGS_METADATA, templates_signature_email
from data.data import TAGS_METADATA
from config.config import settings
from database import (
User,
init_db,
async_session_factory,
get_session,
@ -58,7 +59,6 @@ from schemas import (
FactureUpdate,
LivraisonCreate,
LivraisonUpdate,
StatutSignature,
ArticleCreate,
Article,
ArticleUpdate,
@ -93,9 +93,10 @@ from core.sage_context import (
from utils.generic_functions import (
_preparer_lignes_document,
universign_envoyer,
universign_statut,
)
from core.dependencies import get_current_user
if os.path.exists("/app"):
LOGS_DIR = FilePath("/app/logs")
else:
@ -976,266 +977,6 @@ async def commande_vers_facture(
raise HTTPException(500, str(e))
@app.get("/admin/signatures/relances-auto", tags=["Admin"])
async def relancer_signatures_automatique(session: AsyncSession = Depends(get_session)):
try:
from datetime import timedelta
date_limite = datetime.now() - timedelta(days=7)
query = select(SignatureLog).where(
SignatureLog.statut.in_(
[StatutSignatureDB.EN_ATTENTE, StatutSignatureDB.ENVOYE]
),
SignatureLog.date_envoi < date_limite,
SignatureLog.nb_relances < 3, # Max 3 relances
)
result = await session.execute(query)
signatures_a_relancer = result.scalars().all()
nb_relances = 0
for signature in signatures_a_relancer:
try:
nb_jours = (datetime.now() - signature.date_envoi).days
jours_restants = 30 - nb_jours # Lien expire après 30 jours
if jours_restants <= 0:
signature.statut = StatutSignatureDB.EXPIRE
continue
template = templates_signature_email["relance_signature"]
type_labels = {
0: "Devis",
10: "Commande",
30: "Bon de Livraison",
60: "Facture",
50: "Avoir",
}
variables = {
"NOM_SIGNATAIRE": signature.nom_signataire,
"TYPE_DOC": type_labels.get(signature.type_document, "Document"),
"NUMERO": signature.document_id,
"NB_JOURS": str(nb_jours),
"JOURS_RESTANTS": str(jours_restants),
"SIGNER_URL": signature.signer_url,
"CONTACT_EMAIL": settings.smtp_from,
}
sujet = template["sujet"]
corps = template["corps_html"]
for var, valeur in variables.items():
sujet = sujet.replace(f"{{{{{var}}}}}", str(valeur))
corps = corps.replace(f"{{{{{var}}}}}", str(valeur))
email_log = EmailLog(
id=str(uuid.uuid4()),
destinataire=signature.email_signataire,
sujet=sujet,
corps_html=corps,
document_ids=signature.document_id,
type_document=signature.type_document,
statut=StatutEmailDB.EN_ATTENTE,
date_creation=datetime.now(),
nb_tentatives=0,
)
session.add(email_log)
email_queue.enqueue(email_log.id)
signature.est_relance = True
signature.nb_relances = (signature.nb_relances or 0) + 1
nb_relances += 1
logger.info(
f" Relance envoyée: {signature.document_id} ({signature.nb_relances}/3)"
)
except Exception as e:
logger.error(f"Erreur relance signature {signature.id}: {e}")
continue
await session.commit()
return {
"success": True,
"signatures_verifiees": len(signatures_a_relancer),
"relances_envoyees": nb_relances,
"message": f"{nb_relances} email(s) de relance envoyé(s)",
}
except Exception as e:
logger.error(f"Erreur relances automatiques: {e}")
raise HTTPException(500, str(e))
@app.get("/signature/universign/status", tags=["Signatures"])
async def statut_signature(docId: str = Query(...)):
try:
async with async_session_factory() as session:
query = select(SignatureLog).where(SignatureLog.document_id == docId)
result = await session.execute(query)
signature_log = result.scalar_one_or_none()
if not signature_log:
raise HTTPException(404, "Signature introuvable")
statut = await universign_statut(signature_log.transaction_id)
return {
"doc_id": docId,
"statut": statut["statut"],
"date_signature": statut.get("date_signature"),
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Erreur statut signature: {e}")
raise HTTPException(500, str(e))
@app.get("/signatures", tags=["Signatures"])
async def lister_signatures(
statut: Optional[StatutSignature] = Query(None),
limit: int = Query(100, le=1000),
session: AsyncSession = Depends(get_session),
):
query = select(SignatureLog).order_by(SignatureLog.date_envoi.desc())
if statut:
statut_db = StatutSignatureDB[statut.value]
query = query.where(SignatureLog.statut == statut_db)
query = query.limit(limit)
result = await session.execute(query)
signatures = result.scalars().all()
return [
{
"id": sig.id,
"document_id": sig.document_id,
"type_document": sig.type_document.value,
"transaction_id": sig.transaction_id,
"signer_url": sig.signer_url,
"email_signataire": sig.email_signataire,
"nom_signataire": sig.nom_signataire,
"statut": sig.statut.value,
"date_envoi": sig.date_envoi.isoformat() if sig.date_envoi else None,
"date_signature": (
sig.date_signature.isoformat() if sig.date_signature else None
),
"est_relance": sig.est_relance,
"nb_relances": sig.nb_relances or 0,
}
for sig in signatures
]
@app.get("/signatures/{transaction_id}/status", tags=["Signatures"])
async def statut_signature_detail(
transaction_id: str, session: AsyncSession = Depends(get_session)
):
query = select(SignatureLog).where(SignatureLog.transaction_id == transaction_id)
result = await session.execute(query)
signature_log = result.scalar_one_or_none()
if not signature_log:
raise HTTPException(404, f"Transaction {transaction_id} introuvable")
statut_universign = await universign_statut(transaction_id)
if statut_universign.get("statut") != "ERREUR":
statut_map = {
"EN_ATTENTE": StatutSignatureDB.EN_ATTENTE,
"ENVOYE": StatutSignatureDB.ENVOYE,
"SIGNE": StatutSignatureDB.SIGNE,
"REFUSE": StatutSignatureDB.REFUSE,
"EXPIRE": StatutSignatureDB.EXPIRE,
}
nouveau_statut = statut_map.get(
statut_universign["statut"], StatutSignatureDB.EN_ATTENTE
)
signature_log.statut = nouveau_statut
if statut_universign.get("date_signature"):
signature_log.date_signature = datetime.fromisoformat(
statut_universign["date_signature"].replace("Z", "+00:00")
)
await session.commit()
return {
"transaction_id": transaction_id,
"document_id": signature_log.document_id,
"statut": signature_log.statut.value,
"email_signataire": signature_log.email_signataire,
"date_envoi": (
signature_log.date_envoi.isoformat() if signature_log.date_envoi else None
),
"date_signature": (
signature_log.date_signature.isoformat()
if signature_log.date_signature
else None
),
"signer_url": signature_log.signer_url,
}
@app.post("/signatures/refresh-all", tags=["Signatures"])
async def rafraichir_statuts_signatures(session: AsyncSession = Depends(get_session)):
query = select(SignatureLog).where(
SignatureLog.statut.in_(
[StatutSignatureDB.EN_ATTENTE, StatutSignatureDB.ENVOYE]
)
)
result = await session.execute(query)
signatures = result.scalars().all()
nb_mises_a_jour = 0
for sig in signatures:
try:
statut_universign = await universign_statut(sig.transaction_id)
if statut_universign.get("statut") != "ERREUR":
statut_map = {
"SIGNE": StatutSignatureDB.SIGNE,
"REFUSE": StatutSignatureDB.REFUSE,
"EXPIRE": StatutSignatureDB.EXPIRE,
}
nouveau = statut_map.get(statut_universign["statut"])
if nouveau and nouveau != sig.statut:
sig.statut = nouveau
if statut_universign.get("date_signature"):
sig.date_signature = datetime.fromisoformat(
statut_universign["date_signature"].replace("Z", "+00:00")
)
nb_mises_a_jour += 1
except Exception as e:
logger.error(f"Erreur refresh signature {sig.transaction_id}: {e}")
continue
await session.commit()
return {
"success": True,
"nb_signatures_verifiees": len(signatures),
"nb_mises_a_jour": nb_mises_a_jour,
}
class EmailBatch(BaseModel):
destinataires: List[EmailStr] = Field(..., min_length=1, max_length=100)
sujet: str = Field(..., min_length=1, max_length=500)
@ -1756,14 +1497,19 @@ class TemplatePreview(BaseModel):
@app.get("/templates/emails", response_model=List[TemplateEmail], tags=["Emails"])
async def lister_templates():
async def lister_templates(
user: User = Depends(get_current_user),
):
return [TemplateEmail(**template) for template in templates_email_db.values()]
@app.get(
"/templates/emails/{template_id}", response_model=TemplateEmail, tags=["Emails"]
)
async def lire_template(template_id: str):
async def lire_template(
template_id: str,
user: User = Depends(get_current_user),
):
if template_id not in templates_email_db:
raise HTTPException(404, f"Template {template_id} introuvable")
@ -1771,7 +1517,10 @@ async def lire_template(template_id: str):
@app.post("/templates/emails", response_model=TemplateEmail, tags=["Emails"])
async def creer_template(template: TemplateEmail):
async def creer_template(
template: TemplateEmail,
user: User = Depends(get_current_user),
):
template_id = str(uuid.uuid4())
templates_email_db[template_id] = {
@ -1790,7 +1539,11 @@ async def creer_template(template: TemplateEmail):
@app.put(
"/templates/emails/{template_id}", response_model=TemplateEmail, tags=["Emails"]
)
async def modifier_template(template_id: str, template: TemplateEmail):
async def modifier_template(
template_id: str,
template: TemplateEmail,
user: User = Depends(get_current_user),
):
if template_id not in templates_email_db:
raise HTTPException(404, f"Template {template_id} introuvable")
@ -1811,7 +1564,10 @@ async def modifier_template(template_id: str, template: TemplateEmail):
@app.delete("/templates/emails/{template_id}", tags=["Emails"])
async def supprimer_template(template_id: str):
async def supprimer_template(
template_id: str,
user: User = Depends(get_current_user),
):
if template_id not in templates_email_db:
raise HTTPException(404, f"Template {template_id} introuvable")
@ -2641,6 +2397,7 @@ async def lister_utilisateurs_debug(
limit: int = Query(100, le=1000),
role: Optional[str] = Query(None),
verified_only: bool = Query(False),
user: User = Depends(get_current_user),
):
from database import User
from sqlalchemy import select

View file

@ -7,6 +7,7 @@ from typing import Optional
import uuid
from database import get_session, User, RefreshToken, LoginAttempt
from core.dependencies import get_current_user
from security.auth import (
hash_password,
verify_password,
@ -19,7 +20,6 @@ from security.auth import (
hash_token,
)
from services.email_service import AuthEmailService
from core.dependencies import get_current_user
from config.config import settings
import logging

View file

@ -1,11 +1,11 @@
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from fastapi.responses import FileResponse
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, and_
from sqlalchemy import select, func
from sqlalchemy.orm import selectinload
from typing import List, Optional
from datetime import datetime, timedelta
from datetime import datetime
import logging
from core.dependencies import get_current_user
from data.data import templates_signature_email
from email_queue import email_queue
from database import UniversignSignerStatus, UniversignTransactionStatus, get_session
@ -32,7 +32,9 @@ from schemas import (
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/universign", tags=["Universign"])
router = APIRouter(
prefix="/universign", tags=["Universign"], dependencies=[Depends(get_current_user)]
)
sync_service = UniversignSyncService(
api_url=settings.universign_api_url, api_key=settings.universign_api_key
@ -494,14 +496,11 @@ async def sync_all_transactions(
return {"success": True, "stats": stats, "timestamp": datetime.now().isoformat()}
@router.post("/webhook")
@router.post("/webhook/")
@router.post("/webhook", dependencies=[])
@router.post("/webhook/", dependencies=[])
async def webhook_universign(
request: Request, session: AsyncSession = Depends(get_session)
):
"""
CORRECTION : Extraction correcte du transaction_id selon la structure réelle d'Universign
"""
try:
payload = await request.json()
@ -1082,159 +1081,6 @@ async def trouver_transactions_inconsistantes(
raise HTTPException(500, str(e))
@router.post("/admin/nettoyer-transactions-erreur", tags=["Admin"])
async def nettoyer_transactions_erreur(
age_jours: int = Query(
7, description="Supprimer les transactions en erreur de plus de X jours"
),
session: AsyncSession = Depends(get_session),
):
try:
date_limite = datetime.now() - timedelta(days=age_jours)
query = select(UniversignTransaction).where(
and_(
UniversignTransaction.local_status == LocalDocumentStatus.ERROR,
UniversignTransaction.created_at < date_limite,
)
)
result = await session.execute(query)
transactions = result.scalars().all()
supprimees = []
for tx in transactions:
supprimees.append(
{
"transaction_id": tx.transaction_id,
"document_id": tx.sage_document_id,
"date_creation": tx.created_at.isoformat(),
"erreur": tx.sync_error,
}
)
await session.delete(tx)
await session.commit()
return {
"success": True,
"transactions_supprimees": len(supprimees),
"age_limite_jours": age_jours,
"details": supprimees,
}
except Exception as e:
logger.error(f"Erreur nettoyage: {e}")
raise HTTPException(500, str(e))
@router.get("/debug/webhook-payload/{transaction_id}", tags=["Debug"])
async def voir_dernier_webhook(
transaction_id: str, session: AsyncSession = Depends(get_session)
):
try:
query = select(UniversignTransaction).where(
UniversignTransaction.transaction_id == transaction_id
)
result = await session.execute(query)
tx = result.scalar_one_or_none()
if not tx:
raise HTTPException(404, "Transaction introuvable")
logs_query = (
select(UniversignSyncLog)
.where(
and_(
UniversignSyncLog.transaction_id == tx.id,
UniversignSyncLog.sync_type.like("webhook:%"),
)
)
.order_by(UniversignSyncLog.sync_timestamp.desc())
.limit(1)
)
logs_result = await session.execute(logs_query)
last_webhook_log = logs_result.scalar_one_or_none()
if not last_webhook_log:
return {
"transaction_id": transaction_id,
"webhook_recu": tx.webhook_received,
"dernier_payload": None,
"message": "Aucun webhook reçu pour cette transaction",
}
return {
"transaction_id": transaction_id,
"webhook_recu": tx.webhook_received,
"dernier_webhook": {
"timestamp": last_webhook_log.sync_timestamp.isoformat(),
"type": last_webhook_log.sync_type,
"success": last_webhook_log.success,
"payload": json.loads(last_webhook_log.changes_detected)
if last_webhook_log.changes_detected
else None,
},
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Erreur debug webhook: {e}")
raise HTTPException(500, str(e))
@router.get(
"/transactions/{transaction_id}/document/download", tags=["Documents Signés"]
)
async def telecharger_document_signe(
transaction_id: str, session: AsyncSession = Depends(get_session)
):
try:
query = select(UniversignTransaction).where(
UniversignTransaction.transaction_id == transaction_id
)
result = await session.execute(query)
transaction = result.scalar_one_or_none()
if not transaction:
raise HTTPException(404, f"Transaction {transaction_id} introuvable")
if not transaction.signed_document_path:
raise HTTPException(
404,
"Document signé non disponible localement. "
"Utilisez POST /admin/download-missing-documents pour le récupérer.",
)
file_path = Path(transaction.signed_document_path)
if not file_path.exists():
logger.warning(f"Fichier perdu : {file_path}")
raise HTTPException(
404,
"Fichier introuvable sur le serveur. "
"Utilisez POST /admin/download-missing-documents pour le récupérer.",
)
download_name = (
f"{transaction.sage_document_id}_"
f"{transaction.sage_document_type.name}_"
f"signe.pdf"
)
return FileResponse(
path=str(file_path), media_type="application/pdf", filename=download_name
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Erreur téléchargement document : {e}", exc_info=True)
raise HTTPException(500, str(e))
@router.get("/transactions/{transaction_id}/document/info", tags=["Documents Signés"])
async def info_document_signe(
transaction_id: str, session: AsyncSession = Depends(get_session)

View file

@ -6,7 +6,7 @@ import httpx
from datetime import datetime
from typing import Optional, Tuple, List
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import false, select, true, update, and_
from sqlalchemy import false, select, update, and_
import logging
from config.config import settings