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 from sqlalchemy import select
import os import os
from pathlib import Path as FilePath 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 config.config import settings
from database import ( from database import (
User,
init_db, init_db,
async_session_factory, async_session_factory,
get_session, get_session,
@ -58,7 +59,6 @@ from schemas import (
FactureUpdate, FactureUpdate,
LivraisonCreate, LivraisonCreate,
LivraisonUpdate, LivraisonUpdate,
StatutSignature,
ArticleCreate, ArticleCreate,
Article, Article,
ArticleUpdate, ArticleUpdate,
@ -93,9 +93,10 @@ from core.sage_context import (
from utils.generic_functions import ( from utils.generic_functions import (
_preparer_lignes_document, _preparer_lignes_document,
universign_envoyer, universign_envoyer,
universign_statut,
) )
from core.dependencies import get_current_user
if os.path.exists("/app"): if os.path.exists("/app"):
LOGS_DIR = FilePath("/app/logs") LOGS_DIR = FilePath("/app/logs")
else: else:
@ -976,266 +977,6 @@ async def commande_vers_facture(
raise HTTPException(500, str(e)) 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): class EmailBatch(BaseModel):
destinataires: List[EmailStr] = Field(..., min_length=1, max_length=100) destinataires: List[EmailStr] = Field(..., min_length=1, max_length=100)
sujet: str = Field(..., min_length=1, max_length=500) 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"]) @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()] return [TemplateEmail(**template) for template in templates_email_db.values()]
@app.get( @app.get(
"/templates/emails/{template_id}", response_model=TemplateEmail, tags=["Emails"] "/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: if template_id not in templates_email_db:
raise HTTPException(404, f"Template {template_id} introuvable") 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"]) @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()) template_id = str(uuid.uuid4())
templates_email_db[template_id] = { templates_email_db[template_id] = {
@ -1790,7 +1539,11 @@ async def creer_template(template: TemplateEmail):
@app.put( @app.put(
"/templates/emails/{template_id}", response_model=TemplateEmail, tags=["Emails"] "/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: if template_id not in templates_email_db:
raise HTTPException(404, f"Template {template_id} introuvable") 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"]) @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: if template_id not in templates_email_db:
raise HTTPException(404, f"Template {template_id} introuvable") raise HTTPException(404, f"Template {template_id} introuvable")
@ -2641,6 +2397,7 @@ async def lister_utilisateurs_debug(
limit: int = Query(100, le=1000), limit: int = Query(100, le=1000),
role: Optional[str] = Query(None), role: Optional[str] = Query(None),
verified_only: bool = Query(False), verified_only: bool = Query(False),
user: User = Depends(get_current_user),
): ):
from database import User from database import User
from sqlalchemy import select from sqlalchemy import select

View file

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

View file

@ -1,11 +1,11 @@
from fastapi import APIRouter, Depends, HTTPException, Query, Request from fastapi import APIRouter, Depends, HTTPException, Query, Request
from fastapi.responses import FileResponse
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, and_ from sqlalchemy import select, func
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from typing import List, Optional from typing import List, Optional
from datetime import datetime, timedelta from datetime import datetime
import logging import logging
from core.dependencies import get_current_user
from data.data import templates_signature_email from data.data import templates_signature_email
from email_queue import email_queue from email_queue import email_queue
from database import UniversignSignerStatus, UniversignTransactionStatus, get_session from database import UniversignSignerStatus, UniversignTransactionStatus, get_session
@ -32,7 +32,9 @@ from schemas import (
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix="/universign", tags=["Universign"]) router = APIRouter(
prefix="/universign", tags=["Universign"], dependencies=[Depends(get_current_user)]
)
sync_service = UniversignSyncService( sync_service = UniversignSyncService(
api_url=settings.universign_api_url, api_key=settings.universign_api_key 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()} return {"success": True, "stats": stats, "timestamp": datetime.now().isoformat()}
@router.post("/webhook") @router.post("/webhook", dependencies=[])
@router.post("/webhook/") @router.post("/webhook/", dependencies=[])
async def webhook_universign( async def webhook_universign(
request: Request, session: AsyncSession = Depends(get_session) request: Request, session: AsyncSession = Depends(get_session)
): ):
"""
CORRECTION : Extraction correcte du transaction_id selon la structure réelle d'Universign
"""
try: try:
payload = await request.json() payload = await request.json()
@ -1082,159 +1081,6 @@ async def trouver_transactions_inconsistantes(
raise HTTPException(500, str(e)) 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"]) @router.get("/transactions/{transaction_id}/document/info", tags=["Documents Signés"])
async def info_document_signe( async def info_document_signe(
transaction_id: str, session: AsyncSession = Depends(get_session) transaction_id: str, session: AsyncSession = Depends(get_session)

View file

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