Compare commits

..

No commits in common. "c1f4c66e8c3cdcefd0e4d3ee3ca2b2a3080cf51a" and "fa95d0d11728e0335ab8ee5b7b2bf1343082352a" have entirely different histories.

7 changed files with 532 additions and 484 deletions

125
api.py
View file

@ -1,5 +1,5 @@
from fastapi import FastAPI, HTTPException, Path, Query, Depends, status, Body from fastapi import FastAPI, HTTPException, Path, Query, Depends, status, Body
from fastapi.openapi.utils import get_openapi from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse, HTMLResponse, Response from fastapi.responses import StreamingResponse, HTMLResponse, Response
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel, Field, EmailStr from pydantic import BaseModel, Field, EmailStr
@ -96,7 +96,7 @@ from utils.generic_functions import (
) )
from middleware.security import SwaggerAuthMiddleware, ApiKeyMiddlewareHTTP from middleware.security import SwaggerAuthMiddleware, ApiKeyMiddleware
from core.dependencies import get_current_user from core.dependencies import get_current_user
from config.cors_config import setup_cors from config.cors_config import setup_cors
from routes.api_keys import router as api_keys_router from routes.api_keys import router as api_keys_router
@ -179,26 +179,12 @@ def custom_openapi():
if app.openapi_schema: if app.openapi_schema:
return app.openapi_schema return app.openapi_schema
openapi_schema = get_openapi( openapi_schema = app.openapi()
title=app.title,
version=app.version,
description=app.description,
routes=app.routes,
)
# Définir deux schémas de sécurité
openapi_schema["components"]["securitySchemes"] = { openapi_schema["components"]["securitySchemes"] = {
"HTTPBearer": { "HTTPBearer": {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"},
"type": "http", "ApiKeyAuth": {"type": "apiKey", "in": "header", "name": "X-API-Key"},
"scheme": "bearer",
"bearerFormat": "JWT",
"description": "Authentification JWT pour utilisateurs (POST /auth/login)",
},
"ApiKeyAuth": {
"type": "apiKey",
"in": "header",
"name": "X-API-Key",
"description": "Clé API pour intégrations externes (format: sdk_live_xxx)",
},
} }
openapi_schema["security"] = [{"HTTPBearer": []}, {"ApiKeyAuth": []}] openapi_schema["security"] = [{"HTTPBearer": []}, {"ApiKeyAuth": []}]
@ -207,12 +193,13 @@ def custom_openapi():
return app.openapi_schema return app.openapi_schema
# Après app = FastAPI(...), ajouter:
app.openapi = custom_openapi app.openapi = custom_openapi
setup_cors(app, mode="open") setup_cors(app, mode="open")
app.add_middleware(SwaggerAuthMiddleware) app.add_middleware(SwaggerAuthMiddleware)
app.add_middleware(ApiKeyMiddlewareHTTP) app.add_middleware(ApiKeyMiddleware)
app.include_router(api_keys_router) app.include_router(api_keys_router)
app.include_router(auth_router) app.include_router(auth_router)
@ -224,6 +211,7 @@ app.include_router(entreprises_router)
@app.get("/clients", response_model=List[ClientDetails], tags=["Clients"]) @app.get("/clients", response_model=List[ClientDetails], tags=["Clients"])
async def obtenir_clients( async def obtenir_clients(
query: Optional[str] = Query(None), query: Optional[str] = Query(None),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -237,6 +225,7 @@ async def obtenir_clients(
@app.get("/clients/{code}", response_model=ClientDetails, tags=["Clients"]) @app.get("/clients/{code}", response_model=ClientDetails, tags=["Clients"])
async def lire_client_detail( async def lire_client_detail(
code: str, code: str,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -259,6 +248,7 @@ async def modifier_client(
code: str, code: str,
client_update: ClientUpdate, client_update: ClientUpdate,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -284,6 +274,7 @@ async def modifier_client(
async def ajouter_client( async def ajouter_client(
client: ClientCreate, client: ClientCreate,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -308,6 +299,7 @@ async def ajouter_client(
@app.get("/articles", response_model=List[Article], tags=["Articles"]) @app.get("/articles", response_model=List[Article], tags=["Articles"])
async def rechercher_articles( async def rechercher_articles(
query: Optional[str] = Query(None), query: Optional[str] = Query(None),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -326,6 +318,7 @@ async def rechercher_articles(
) )
async def creer_article( async def creer_article(
article: ArticleCreate, article: ArticleCreate,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -366,6 +359,7 @@ async def creer_article(
async def modifier_article( async def modifier_article(
reference: str = Path(..., description="Référence de l'article à modifier"), reference: str = Path(..., description="Référence de l'article à modifier"),
article: ArticleUpdate = Body(...), article: ArticleUpdate = Body(...),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -409,6 +403,7 @@ async def modifier_article(
@app.get("/articles/{reference}", response_model=Article, tags=["Articles"]) @app.get("/articles/{reference}", response_model=Article, tags=["Articles"])
async def lire_article( async def lire_article(
reference: str = Path(..., description="Référence de l'article"), reference: str = Path(..., description="Référence de l'article"),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -438,6 +433,7 @@ async def lire_article(
@app.post("/devis", response_model=Devis, status_code=201, tags=["Devis"]) @app.post("/devis", response_model=Devis, status_code=201, tags=["Devis"])
async def creer_devis( async def creer_devis(
devis: DevisRequest, devis: DevisRequest,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -477,6 +473,7 @@ async def modifier_devis(
id: str, id: str,
devis_update: DevisUpdate, devis_update: DevisUpdate,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -522,6 +519,7 @@ async def modifier_devis(
async def creer_commande( async def creer_commande(
commande: CommandeCreate, commande: CommandeCreate,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -571,6 +569,7 @@ async def modifier_commande(
id: str, id: str,
commande_update: CommandeUpdate, commande_update: CommandeUpdate,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -619,6 +618,7 @@ async def lister_devis(
inclure_lignes: bool = Query( inclure_lignes: bool = Query(
True, description="Inclure les lignes de chaque devis" True, description="Inclure les lignes de chaque devis"
), ),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -635,6 +635,7 @@ async def lister_devis(
@app.get("/devis/{id}", tags=["Devis"]) @app.get("/devis/{id}", tags=["Devis"])
async def lire_devis( async def lire_devis(
id: str, id: str,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -655,6 +656,7 @@ async def lire_devis(
@app.get("/devis/{id}/pdf", tags=["Devis"]) @app.get("/devis/{id}/pdf", tags=["Devis"])
async def telecharger_devis_pdf( async def telecharger_devis_pdf(
id: str, id: str,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -677,6 +679,7 @@ async def telecharger_document_pdf(
description="Type de document (0=Devis, 10=Commande, 30=Livraison, 60=Facture, 50=Avoir)", description="Type de document (0=Devis, 10=Commande, 30=Livraison, 60=Facture, 50=Avoir)",
), ),
numero: str = Path(..., description="Numéro du document"), numero: str = Path(..., description="Numéro du document"),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -733,6 +736,7 @@ async def envoyer_devis_email(
id: str, id: str,
request: EmailEnvoi, request: EmailEnvoi,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -788,6 +792,7 @@ async def changer_statut_document(
nouveau_statut: int = Query( nouveau_statut: int = Query(
..., ge=0, le=6, description="0=Saisi, 1=Confirmé, 2=Accepté" ..., ge=0, le=6, description="0=Saisi, 1=Confirmé, 2=Accepté"
), ),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
document_type_sql = None document_type_sql = None
@ -904,6 +909,7 @@ async def changer_statut_document(
@app.get("/commandes/{id}", tags=["Commandes"]) @app.get("/commandes/{id}", tags=["Commandes"])
async def lire_commande( async def lire_commande(
id: str, id: str,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -922,6 +928,7 @@ async def lire_commande(
async def lister_commandes( async def lister_commandes(
limit: int = Query(100, le=1000), limit: int = Query(100, le=1000),
statut: Optional[int] = Query(None), statut: Optional[int] = Query(None),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -937,6 +944,7 @@ async def lister_commandes(
async def devis_vers_commande( async def devis_vers_commande(
id: str, id: str,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -981,6 +989,7 @@ async def devis_vers_commande(
async def commande_vers_facture( async def commande_vers_facture(
id: str, id: str,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -1082,6 +1091,7 @@ async def envoyer_emails_lot(
async def valider_remise( async def valider_remise(
client_id: str = Query(..., min_length=1), client_id: str = Query(..., min_length=1),
remise_pourcentage: float = Query(0.0, ge=0, le=100), remise_pourcentage: float = Query(0.0, ge=0, le=100),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -1115,6 +1125,7 @@ async def relancer_devis_signature(
id: str, id: str,
relance: RelanceDevis, relance: RelanceDevis,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -1181,6 +1192,7 @@ class ContactClientResponse(BaseModel):
@app.get("/devis/{id}/contact", response_model=ContactClientResponse, tags=["Devis"]) @app.get("/devis/{id}/contact", response_model=ContactClientResponse, tags=["Devis"])
async def recuperer_contact_devis( async def recuperer_contact_devis(
id: str, id: str,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -1208,6 +1220,7 @@ async def recuperer_contact_devis(
async def lister_factures( async def lister_factures(
limit: int = Query(100, le=1000), limit: int = Query(100, le=1000),
statut: Optional[int] = Query(None), statut: Optional[int] = Query(None),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -1222,6 +1235,7 @@ async def lister_factures(
@app.get("/factures/{numero}", tags=["Factures"]) @app.get("/factures/{numero}", tags=["Factures"])
async def lire_facture_detail( async def lire_facture_detail(
numero: str, numero: str,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -1248,6 +1262,7 @@ class RelanceFacture(BaseModel):
async def creer_facture( async def creer_facture(
facture: FactureCreate, facture: FactureCreate,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -1297,6 +1312,7 @@ async def modifier_facture(
id: str, id: str,
facture_update: FactureUpdate, facture_update: FactureUpdate,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -1366,6 +1382,7 @@ async def relancer_facture(
id: str, id: str,
relance: RelanceFacture, relance: RelanceFacture,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -1436,6 +1453,7 @@ async def journal_emails(
destinataire: Optional[str] = Query(None), destinataire: Optional[str] = Query(None),
limit: int = Query(100, le=1000), limit: int = Query(100, le=1000),
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
query = select(EmailLog) query = select(EmailLog)
@ -1471,6 +1489,7 @@ async def journal_emails(
async def exporter_logs_csv( async def exporter_logs_csv(
statut: Optional[StatutEmail] = Query(None), statut: Optional[StatutEmail] = Query(None),
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
query = select(EmailLog) query = select(EmailLog)
@ -1540,7 +1559,9 @@ 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()]
@ -1549,6 +1570,7 @@ async def lister_templates():
) )
async def lire_template( async def lire_template(
template_id: str, 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")
@ -1559,6 +1581,7 @@ async def lire_template(
@app.post("/templates/emails", response_model=TemplateEmail, tags=["Emails"]) @app.post("/templates/emails", response_model=TemplateEmail, tags=["Emails"])
async def creer_template( async def creer_template(
template: TemplateEmail, template: TemplateEmail,
user: User = Depends(get_current_user),
): ):
template_id = str(uuid.uuid4()) template_id = str(uuid.uuid4())
@ -1581,6 +1604,7 @@ async def creer_template(
async def modifier_template( async def modifier_template(
template_id: str, template_id: str,
template: TemplateEmail, 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")
@ -1604,6 +1628,7 @@ async def modifier_template(
@app.delete("/templates/emails/{template_id}", tags=["Emails"]) @app.delete("/templates/emails/{template_id}", tags=["Emails"])
async def supprimer_template( async def supprimer_template(
template_id: str, 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")
@ -1621,6 +1646,7 @@ async def supprimer_template(
@app.post("/templates/emails/preview", tags=["Emails"]) @app.post("/templates/emails/preview", tags=["Emails"])
async def previsualiser_email( async def previsualiser_email(
preview: TemplatePreview, preview: TemplatePreview,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
if preview.template_id not in templates_email_db: if preview.template_id not in templates_email_db:
@ -1659,6 +1685,7 @@ async def previsualiser_email(
@app.get("/prospects", tags=["Prospects"]) @app.get("/prospects", tags=["Prospects"])
async def rechercher_prospects( async def rechercher_prospects(
query: Optional[str] = Query(None), query: Optional[str] = Query(None),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -1672,6 +1699,7 @@ async def rechercher_prospects(
@app.get("/prospects/{code}", tags=["Prospects"]) @app.get("/prospects/{code}", tags=["Prospects"])
async def lire_prospect( async def lire_prospect(
code: str, code: str,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -1691,6 +1719,7 @@ async def lire_prospect(
) )
async def rechercher_fournisseurs( async def rechercher_fournisseurs(
query: Optional[str] = Query(None), query: Optional[str] = Query(None),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -1712,6 +1741,7 @@ async def rechercher_fournisseurs(
async def ajouter_fournisseur( async def ajouter_fournisseur(
fournisseur: FournisseurCreate, fournisseur: FournisseurCreate,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -1741,6 +1771,7 @@ async def modifier_fournisseur(
code: str, code: str,
fournisseur_update: FournisseurUpdate, fournisseur_update: FournisseurUpdate,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -1763,6 +1794,7 @@ async def modifier_fournisseur(
@app.get("/fournisseurs/{code}", tags=["Fournisseurs"]) @app.get("/fournisseurs/{code}", tags=["Fournisseurs"])
async def lire_fournisseur( async def lire_fournisseur(
code: str, code: str,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -1781,6 +1813,7 @@ async def lire_fournisseur(
async def lister_avoirs( async def lister_avoirs(
limit: int = Query(100, le=1000), limit: int = Query(100, le=1000),
statut: Optional[int] = Query(None), statut: Optional[int] = Query(None),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -1794,6 +1827,7 @@ async def lister_avoirs(
@app.get("/avoirs/{numero}", tags=["Avoirs"]) @app.get("/avoirs/{numero}", tags=["Avoirs"])
async def lire_avoir( async def lire_avoir(
numero: str, numero: str,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -1812,6 +1846,7 @@ async def lire_avoir(
async def creer_avoir( async def creer_avoir(
avoir: AvoirCreate, avoir: AvoirCreate,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -1859,6 +1894,7 @@ async def modifier_avoir(
id: str, id: str,
avoir_update: AvoirUpdate, avoir_update: AvoirUpdate,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -1904,6 +1940,7 @@ async def modifier_avoir(
async def lister_livraisons( async def lister_livraisons(
limit: int = Query(100, le=1000), limit: int = Query(100, le=1000),
statut: Optional[int] = Query(None), statut: Optional[int] = Query(None),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -1917,6 +1954,7 @@ async def lister_livraisons(
@app.get("/livraisons/{numero}", tags=["Livraisons"]) @app.get("/livraisons/{numero}", tags=["Livraisons"])
async def lire_livraison( async def lire_livraison(
numero: str, numero: str,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -1935,6 +1973,7 @@ async def lire_livraison(
async def creer_livraison( async def creer_livraison(
livraison: LivraisonCreate, livraison: LivraisonCreate,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -1988,6 +2027,7 @@ async def modifier_livraison(
id: str, id: str,
livraison_update: LivraisonUpdate, livraison_update: LivraisonUpdate,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -2033,6 +2073,7 @@ async def modifier_livraison(
async def livraison_vers_facture( async def livraison_vers_facture(
id: str, id: str,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -2076,6 +2117,7 @@ async def livraison_vers_facture(
async def devis_vers_facture_direct( async def devis_vers_facture_direct(
id: str, id: str,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -2136,6 +2178,7 @@ async def devis_vers_facture_direct(
async def commande_vers_livraison( async def commande_vers_livraison(
id: str, id: str,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -2207,6 +2250,7 @@ async def commande_vers_livraison(
) )
async def lister_familles( async def lister_familles(
filtre: Optional[str] = Query(None, description="Filtre sur code ou intitulé"), filtre: Optional[str] = Query(None, description="Filtre sur code ou intitulé"),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -2232,6 +2276,7 @@ async def lister_familles(
) )
async def lire_famille( async def lire_famille(
code: str = Path(..., description="Code de la famille (ex: ZDIVERS)"), code: str = Path(..., description="Code de la famille (ex: ZDIVERS)"),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -2267,6 +2312,7 @@ async def lire_famille(
) )
async def creer_famille( async def creer_famille(
famille: FamilleCreate, famille: FamilleCreate,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -2310,6 +2356,7 @@ async def creer_famille(
) )
async def creer_entree_stock( async def creer_entree_stock(
entree: EntreeStock, entree: EntreeStock,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -2346,6 +2393,7 @@ async def creer_entree_stock(
) )
async def creer_sortie_stock( async def creer_sortie_stock(
sortie: SortieStock, sortie: SortieStock,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -2381,6 +2429,7 @@ async def creer_sortie_stock(
) )
async def lire_mouvement_stock( async def lire_mouvement_stock(
numero: str = Path(..., description="Numéro du mouvement (ex: ME00123 ou MS00124)"), numero: str = Path(..., description="Numéro du mouvement (ex: ME00123 ou MS00124)"),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -2413,6 +2462,7 @@ async def lire_mouvement_stock(
summary="Statistiques sur les familles", summary="Statistiques sur les familles",
) )
async def statistiques_familles( async def statistiques_familles(
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -2434,6 +2484,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
@ -2520,6 +2571,7 @@ async def statistiques_utilisateurs(session: AsyncSession = Depends(get_session)
async def creer_contact( async def creer_contact(
numero: str, numero: str,
contact: ContactCreate, contact: ContactCreate,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -2552,6 +2604,7 @@ async def creer_contact(
@app.get("/tiers/{numero}/contacts", response_model=List[Contact], tags=["Contacts"]) @app.get("/tiers/{numero}/contacts", response_model=List[Contact], tags=["Contacts"])
async def lister_contacts( async def lister_contacts(
numero: str, numero: str,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -2570,6 +2623,7 @@ async def lister_contacts(
async def obtenir_contact( async def obtenir_contact(
numero: str, numero: str,
contact_numero: int, contact_numero: int,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -2595,6 +2649,7 @@ async def modifier_contact(
numero: str, numero: str,
contact_numero: int, contact_numero: int,
contact: ContactUpdate, contact: ContactUpdate,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -2626,6 +2681,7 @@ async def modifier_contact(
async def supprimer_contact( async def supprimer_contact(
numero: str, numero: str,
contact_numero: int, contact_numero: int,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -2640,6 +2696,7 @@ async def supprimer_contact(
async def definir_contact_defaut( async def definir_contact_defaut(
numero: str, numero: str,
contact_numero: int, contact_numero: int,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -2661,6 +2718,7 @@ async def obtenir_tiers(
description="Filtre par type: 0/client, 1/fournisseur, 2/prospect, 3/all ou strings", description="Filtre par type: 0/client, 1/fournisseur, 2/prospect, 3/all ou strings",
), ),
query: Optional[str] = Query(None, description="Recherche sur code ou intitulé"), query: Optional[str] = Query(None, description="Recherche sur code ou intitulé"),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -2675,6 +2733,7 @@ async def obtenir_tiers(
@app.get("/tiers/{code}", response_model=TiersDetails, tags=["Tiers"]) @app.get("/tiers/{code}", response_model=TiersDetails, tags=["Tiers"])
async def lire_tiers_detail( async def lire_tiers_detail(
code: str, code: str,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -2712,6 +2771,7 @@ async def lister_collaborateurs(
actifs_seulement: bool = Query( actifs_seulement: bool = Query(
True, description="Exclure les collaborateurs en sommeil" True, description="Exclure les collaborateurs en sommeil"
), ),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
"""Liste tous les collaborateurs""" """Liste tous les collaborateurs"""
@ -2730,6 +2790,7 @@ async def lister_collaborateurs(
) )
async def lire_collaborateur_detail( async def lire_collaborateur_detail(
numero: int, numero: int,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
"""Lit un collaborateur par son numéro""" """Lit un collaborateur par son numéro"""
@ -2756,6 +2817,7 @@ async def lire_collaborateur_detail(
) )
async def creer_collaborateur( async def creer_collaborateur(
collaborateur: CollaborateurCreate, collaborateur: CollaborateurCreate,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
"""Crée un nouveau collaborateur""" """Crée un nouveau collaborateur"""
@ -2782,6 +2844,7 @@ async def creer_collaborateur(
async def modifier_collaborateur( async def modifier_collaborateur(
numero: int, numero: int,
collaborateur: CollaborateurUpdate, collaborateur: CollaborateurUpdate,
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
"""Modifie un collaborateur existant""" """Modifie un collaborateur existant"""
@ -2804,6 +2867,7 @@ async def modifier_collaborateur(
@app.get("/societe/info", response_model=SocieteInfo, tags=["Société"]) @app.get("/societe/info", response_model=SocieteInfo, tags=["Société"])
async def obtenir_informations_societe( async def obtenir_informations_societe(
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -2823,6 +2887,7 @@ async def obtenir_informations_societe(
@app.get("/societe/logo", tags=["Société"]) @app.get("/societe/logo", tags=["Société"])
async def obtenir_logo_societe( async def obtenir_logo_societe(
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
"""Retourne le logo en tant qu'image directe""" """Retourne le logo en tant qu'image directe"""
@ -2847,6 +2912,7 @@ async def obtenir_logo_societe(
@app.get("/societe/preview", response_class=HTMLResponse, tags=["Société"]) @app.get("/societe/preview", response_class=HTMLResponse, tags=["Société"])
async def preview_societe( async def preview_societe(
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
"""Page HTML pour visualiser les infos société avec logo""" """Page HTML pour visualiser les infos société avec logo"""
@ -2920,6 +2986,7 @@ async def preview_societe(
async def valider_facture( async def valider_facture(
numero_facture: str, numero_facture: str,
_: AsyncSession = Depends(get_session), _: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -2943,6 +3010,7 @@ async def valider_facture(
async def devalider_facture( async def devalider_facture(
numero_facture: str, numero_facture: str,
_: AsyncSession = Depends(get_session), _: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -2966,6 +3034,7 @@ async def devalider_facture(
async def get_statut_validation_facture( async def get_statut_validation_facture(
numero_facture: str, numero_facture: str,
_: AsyncSession = Depends(get_session), _: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -2986,6 +3055,7 @@ async def regler_facture(
numero_facture: str, numero_facture: str,
reglement: ReglementFactureCreate, reglement: ReglementFactureCreate,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -3029,6 +3099,7 @@ async def regler_facture(
async def regler_factures_multiple( async def regler_factures_multiple(
reglement: ReglementMultipleCreate, reglement: ReglementMultipleCreate,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -3067,6 +3138,7 @@ async def regler_factures_multiple(
async def get_reglements_facture( async def get_reglements_facture(
numero_facture: str, numero_facture: str,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -3091,6 +3163,7 @@ async def get_reglements_client(
date_fin: Optional[datetime] = Query(None, description="Date fin"), date_fin: Optional[datetime] = Query(None, description="Date fin"),
inclure_soldees: bool = Query(True, description="Inclure les factures soldées"), inclure_soldees: bool = Query(True, description="Inclure les factures soldées"),
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -3115,6 +3188,7 @@ async def get_reglements_client(
@app.get("/journaux/banque", tags=["Règlements"]) @app.get("/journaux/banque", tags=["Règlements"])
async def get_journaux_banque( async def get_journaux_banque(
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
@ -3127,6 +3201,7 @@ async def get_journaux_banque(
@app.get("/reglements/modes", tags=["Référentiels"]) @app.get("/reglements/modes", tags=["Référentiels"])
async def get_modes_reglement( async def get_modes_reglement(
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
"""Liste des modes de règlement disponibles dans Sage""" """Liste des modes de règlement disponibles dans Sage"""
@ -3140,6 +3215,7 @@ async def get_modes_reglement(
@app.get("/devises", tags=["Référentiels"]) @app.get("/devises", tags=["Référentiels"])
async def get_devises( async def get_devises(
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
"""Liste des devises disponibles dans Sage""" """Liste des devises disponibles dans Sage"""
@ -3153,6 +3229,7 @@ async def get_devises(
@app.get("/journaux/tresorerie", tags=["Référentiels"]) @app.get("/journaux/tresorerie", tags=["Référentiels"])
async def get_journaux_tresorerie( async def get_journaux_tresorerie(
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
"""Liste des journaux de trésorerie (banque + caisse)""" """Liste des journaux de trésorerie (banque + caisse)"""
@ -3171,6 +3248,7 @@ async def get_comptes_generaux(
None, None,
description="client | fournisseur | banque | caisse | tva | produit | charge", description="client | fournisseur | banque | caisse | tva | produit | charge",
), ),
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
"""Liste des comptes généraux""" """Liste des comptes généraux"""
@ -3184,6 +3262,7 @@ async def get_comptes_generaux(
@app.get("/tva/taux", tags=["Référentiels"]) @app.get("/tva/taux", tags=["Référentiels"])
async def get_tva_taux( async def get_tva_taux(
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
"""Liste des taux de TVA""" """Liste des taux de TVA"""
@ -3197,6 +3276,7 @@ async def get_tva_taux(
@app.get("/parametres/encaissement", tags=["Référentiels"]) @app.get("/parametres/encaissement", tags=["Référentiels"])
async def get_parametres_encaissement( async def get_parametres_encaissement(
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
"""Paramètres TVA sur encaissement""" """Paramètres TVA sur encaissement"""
@ -3243,6 +3323,7 @@ async def get_reglement_detail(rg_no):
@app.get("/health", tags=["System"]) @app.get("/health", tags=["System"])
async def health_check( async def health_check(
user: User = Depends(get_current_user),
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
gateway_health = sage.health() gateway_health = sage.health()

View file

@ -2,11 +2,13 @@ from fastapi import Depends, HTTPException, status, Request
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
from typing import Optional
from jwt.exceptions import InvalidTokenError
from database import get_session, User from database import get_session, User
from security.auth import decode_token from security.auth import decode_token
from typing import Optional
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
security = HTTPBearer(auto_error=False) security = HTTPBearer(auto_error=False)
@ -16,6 +18,62 @@ async def get_current_user_hybrid(
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security), credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
) -> User: ) -> User:
if credentials and credentials.credentials:
token = credentials.credentials
payload = decode_token(token)
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token invalide ou expiré",
headers={"WWW-Authenticate": "Bearer"},
)
if payload.get("type") != "access":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Type de token incorrect",
headers={"WWW-Authenticate": "Bearer"},
)
user_id: str = payload.get("sub")
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token malformé",
headers={"WWW-Authenticate": "Bearer"},
)
result = await session.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Utilisateur introuvable",
headers={"WWW-Authenticate": "Bearer"},
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Compte désactivé"
)
if not user.is_verified:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Email non vérifié. Consultez votre boîte de réception.",
)
if user.locked_until and user.locked_until > datetime.now():
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Compte temporairement verrouillé suite à trop de tentatives échouées",
)
logger.debug(f" Authentifié via JWT: {user.email}")
return user
api_key_obj = getattr(request.state, "api_key", None) api_key_obj = getattr(request.state, "api_key", None)
if api_key_obj: if api_key_obj:
@ -26,81 +84,71 @@ async def get_current_user_hybrid(
user = result.scalar_one_or_none() user = result.scalar_one_or_none()
if user: if user:
user._is_api_key_user = True logger.debug(
user._api_key_obj = api_key_obj f" Authentifié via API Key ({api_key_obj.name}) → User: {user.email}"
)
return user return user
virtual_user = User( from database import User as UserModel
virtual_user = UserModel(
id=f"api_key_{api_key_obj.id}", id=f"api_key_{api_key_obj.id}",
email=f"api_key_{api_key_obj.id}@virtual.local", email=f"{api_key_obj.name.lower().replace(' ', '_')}@api-key.local",
nom=api_key_obj.name, nom="API Key",
prenom="API", prenom=api_key_obj.name,
hashed_password="",
role="api_client", role="api_client",
is_active=True,
is_verified=True, is_verified=True,
is_active=True,
) )
virtual_user._is_api_key_user = True virtual_user._is_api_key_user = True
virtual_user._api_key_obj = api_key_obj virtual_user._api_key_obj = api_key_obj
logger.debug(f" Authentifié via API Key: {api_key_obj.name} (user virtuel)")
return virtual_user return virtual_user
if not credentials: raise HTTPException(
raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED,
status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentification requise (JWT ou API Key)",
detail="Authentification requise (JWT ou API Key)", headers={"WWW-Authenticate": "Bearer"},
headers={"WWW-Authenticate": "Bearer"}, )
)
token = credentials.credentials
async def get_current_user_optional_hybrid(
request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
session: AsyncSession = Depends(get_session),
) -> Optional[User]:
"""Version optionnelle de get_current_user_hybrid (ne lève pas d'erreur)"""
try: try:
payload = decode_token(token) return await get_current_user_hybrid(request, credentials, session)
user_id: str = payload.get("sub") except HTTPException:
return None
if user_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token invalide: user_id manquant",
headers={"WWW-Authenticate": "Bearer"},
)
result = await session.execute(select(User).where(User.id == user_id)) def require_role(*allowed_roles: str):
user = result.scalar_one_or_none() async def role_checker(
request: Request, user: User = Depends(get_current_user_hybrid)
) -> User:
is_api_key_user = getattr(user, "_is_api_key_user", False)
if user is None: if is_api_key_user:
raise HTTPException( if "api_client" not in allowed_roles and "*" not in allowed_roles:
status_code=status.HTTP_401_UNAUTHORIZED, raise HTTPException(
detail="Utilisateur introuvable", status_code=status.HTTP_403_FORBIDDEN,
headers={"WWW-Authenticate": "Bearer"}, detail=f"Accès refusé. Rôles requis: {', '.join(allowed_roles)}. API Keys non autorisées.",
) )
logger.debug(" API Key autorisée pour cette route")
return user
if not user.is_active: if user.role not in allowed_roles and "*" not in allowed_roles:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="Utilisateur inactif", detail=f"Accès refusé. Rôles requis: {', '.join(allowed_roles)}",
) )
return user return user
except InvalidTokenError as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Token invalide: {str(e)}",
headers={"WWW-Authenticate": "Bearer"},
)
def require_role_hybrid(*allowed_roles: str):
async def role_checker(user: User = Depends(get_current_user_hybrid)) -> User:
if user.role not in allowed_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Accès interdit. Rôles autorisés: {', '.join(allowed_roles)}",
)
return user
return role_checker return role_checker
@ -110,9 +158,9 @@ def is_api_key_user(user: User) -> bool:
def get_api_key_from_user(user: User): def get_api_key_from_user(user: User):
"""Récupère l'objet ApiKey depuis un utilisateur (si applicable)""" """Récupère l'objet API Key depuis un user virtuel"""
return getattr(user, "_api_key_obj", None) return getattr(user, "_api_key_obj", None)
get_current_user = get_current_user_hybrid get_current_user = get_current_user_hybrid
require_role = require_role_hybrid get_current_user_optional = get_current_user_optional_hybrid

View file

@ -1,14 +1,14 @@
import os
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.pool import NullPool from sqlalchemy.pool import NullPool
from sqlalchemy import event, text from sqlalchemy import event, text
import logging import logging
from config.config import settings
from database.models.generic_model import Base from database.models.generic_model import Base
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
DATABASE_URL = settings.database_url DATABASE_URL = os.getenv("DATABASE_URL")
def _configure_sqlite_connection(dbapi_connection, connection_record): def _configure_sqlite_connection(dbapi_connection, connection_record):

View file

@ -1,23 +1,49 @@
from fastapi import Request, status from fastapi import Request, status
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.security import HTTPBasic, HTTPBasicCredentials
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.types import ASGIApp
from sqlalchemy import select from sqlalchemy import select
from typing import Callable from typing import Optional
from datetime import datetime from datetime import datetime
import logging import logging
import base64
from database import get_session
from database.models.api_key import SwaggerUser
from security.auth import verify_password
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
security = HTTPBasic() security = HTTPBasic()
class SwaggerAuthMiddleware: async def verify_swagger_credentials(credentials: HTTPBasicCredentials) -> bool:
PROTECTED_PATHS = ["/docs", "/redoc", "/openapi.json"] username = credentials.username
password = credentials.password
def __init__(self, app: ASGIApp): try:
async for session in get_session():
result = await session.execute(
select(SwaggerUser).where(SwaggerUser.username == username)
)
swagger_user = result.scalar_one_or_none()
if swagger_user and swagger_user.is_active:
if verify_password(password, swagger_user.hashed_password):
swagger_user.last_login = datetime.now()
await session.commit()
logger.info(f" Accès Swagger autorisé (DB): {username}")
return True
logger.warning(f" Tentative d'accès Swagger refusée: {username}")
return False
except Exception as e:
logger.error(f" Erreur vérification Swagger credentials: {e}")
return False
class SwaggerAuthMiddleware:
def __init__(self, app):
self.app = app self.app = app
async def __call__(self, scope, receive, send): async def __call__(self, scope, receive, send):
@ -28,240 +54,183 @@ class SwaggerAuthMiddleware:
request = Request(scope, receive=receive) request = Request(scope, receive=receive)
path = request.url.path path = request.url.path
if not any(path.startswith(p) for p in self.PROTECTED_PATHS): protected_paths = ["/docs", "/redoc", "/openapi.json"]
await self.app(scope, receive, send)
return
auth_header = request.headers.get("Authorization") if any(path.startswith(protected_path) for protected_path in protected_paths):
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Basic "): if not auth_header or not auth_header.startswith("Basic "):
response = JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "Authentification requise pour la documentation"},
headers={"WWW-Authenticate": 'Basic realm="Swagger UI"'},
)
await response(scope, receive, send)
return
try:
encoded_credentials = auth_header.split(" ")[1]
decoded_credentials = base64.b64decode(encoded_credentials).decode("utf-8")
username, password = decoded_credentials.split(":", 1)
credentials = HTTPBasicCredentials(username=username, password=password)
if not await self._verify_credentials(credentials):
response = JSONResponse( response = JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "Identifiants invalides"}, content={
"detail": "Authentification requise pour accéder à la documentation"
},
headers={"WWW-Authenticate": 'Basic realm="Swagger UI"'}, headers={"WWW-Authenticate": 'Basic realm="Swagger UI"'},
) )
await response(scope, receive, send) await response(scope, receive, send)
return return
except Exception as e: try:
logger.error(f"Erreur parsing auth header: {e}") import base64
encoded_credentials = auth_header.split(" ")[1]
decoded_credentials = base64.b64decode(encoded_credentials).decode(
"utf-8"
)
username, password = decoded_credentials.split(":", 1)
credentials = HTTPBasicCredentials(username=username, password=password)
if not await verify_swagger_credentials(credentials):
response = JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "Identifiants invalides"},
headers={"WWW-Authenticate": 'Basic realm="Swagger UI"'},
)
await response(scope, receive, send)
return
except Exception as e:
logger.error(f" Erreur parsing auth header: {e}")
response = JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "Format d'authentification invalide"},
headers={"WWW-Authenticate": 'Basic realm="Swagger UI"'},
)
await response(scope, receive, send)
return
await self.app(scope, receive, send)
class ApiKeyMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
await self.app(scope, receive, send)
return
request = Request(scope, receive=receive)
path = request.url.path
excluded_paths = [
"/docs",
"/redoc",
"/openapi.json",
"/health",
"/",
"/auth/login",
"/auth/register",
"/auth/verify-email",
"/auth/reset-password",
"/auth/request-reset",
"/auth/refresh",
]
if any(path.startswith(excluded_path) for excluded_path in excluded_paths):
await self.app(scope, receive, send)
return
auth_header = request.headers.get("Authorization")
has_jwt = auth_header and auth_header.startswith("Bearer ")
api_key = request.headers.get("X-API-Key")
has_api_key = api_key is not None
if has_jwt:
logger.debug(f" JWT détecté pour {path}")
await self.app(scope, receive, send)
return
elif has_api_key:
logger.debug(f" API Key détectée pour {path}")
from services.api_key import ApiKeyService
try:
async for session in get_session():
api_key_service = ApiKeyService(session)
api_key_obj = await api_key_service.verify_api_key(api_key)
if not api_key_obj:
response = JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={
"detail": "Clé API invalide ou expirée",
"hint": "Utilisez X-API-Key: sdk_live_xxx ou Authorization: Bearer <jwt>",
},
)
await response(scope, receive, send)
return
is_allowed, rate_info = await api_key_service.check_rate_limit(
api_key_obj
)
if not is_allowed:
response = JSONResponse(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
content={"detail": "Rate limit dépassé"},
headers={
"X-RateLimit-Limit": str(rate_info["limit"]),
"X-RateLimit-Remaining": "0",
},
)
await response(scope, receive, send)
return
has_access = await api_key_service.check_endpoint_access(
api_key_obj, path
)
if not has_access:
response = JSONResponse(
status_code=status.HTTP_403_FORBIDDEN,
content={
"detail": "Accès non autorisé à cet endpoint",
"endpoint": path,
"api_key": api_key_obj.key_prefix + "...",
},
)
await response(scope, receive, send)
return
request.state.api_key = api_key_obj
request.state.authenticated_via = "api_key"
logger.info(f" API Key valide: {api_key_obj.name}{path}")
await self.app(scope, receive, send)
return
except Exception as e:
logger.error(f" Erreur validation API Key: {e}", exc_info=True)
response = JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={
"detail": "Erreur interne lors de la validation de la clé"
},
)
await response(scope, receive, send)
return
else:
response = JSONResponse( response = JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "Format d'authentification invalide"}, content={
headers={"WWW-Authenticate": 'Basic realm="Swagger UI"'}, "detail": "Authentification requise",
"hint": "Utilisez soit 'X-API-Key: sdk_live_xxx' soit 'Authorization: Bearer <jwt>'",
},
headers={"WWW-Authenticate": 'Bearer realm="API", charset="UTF-8"'},
) )
await response(scope, receive, send) await response(scope, receive, send)
return return
await self.app(scope, receive, send)
async def _verify_credentials(self, credentials: HTTPBasicCredentials) -> bool: def get_api_key_from_request(request: Request) -> Optional:
"""Vérifie les identifiants dans la base de données"""
from database.db_config import async_session_factory
from database.models.api_key import SwaggerUser
from security.auth import verify_password
try:
async with async_session_factory() as session:
result = await session.execute(
select(SwaggerUser).where(
SwaggerUser.username == credentials.username
)
)
swagger_user = result.scalar_one_or_none()
if swagger_user and swagger_user.is_active:
if verify_password(
credentials.password, swagger_user.hashed_password
):
swagger_user.last_login = datetime.now()
await session.commit()
logger.info(f"✓ Accès Swagger autorisé: {credentials.username}")
return True
logger.warning(f"✗ Accès Swagger refusé: {credentials.username}")
return False
except Exception as e:
logger.error(f"Erreur vérification credentials: {e}")
return False
class ApiKeyMiddlewareHTTP(BaseHTTPMiddleware):
EXCLUDED_PATHS = [
"/docs",
"/redoc",
"/openapi.json",
"/",
"/health",
"/auth",
"/api-keys/verify",
"/universign/webhook",
]
def _is_excluded_path(self, path: str) -> bool:
"""Vérifie si le chemin est exclu de l'authentification"""
if path == "/":
return True
for excluded in self.EXCLUDED_PATHS:
if excluded == "/":
continue
if path == excluded or path.startswith(excluded + "/"):
return True
return False
async def dispatch(self, request: Request, call_next: Callable):
path = request.url.path
method = request.method
if self._is_excluded_path(path):
return await call_next(request)
auth_header = request.headers.get("Authorization")
api_key_header = request.headers.get("X-API-Key")
if api_key_header:
logger.debug(f"🔑 API Key détectée pour {method} {path}")
return await self._handle_api_key_auth(
request, api_key_header, path, method, call_next
)
if auth_header and auth_header.startswith("Bearer "):
token = auth_header.split(" ")[1]
if token.startswith("sdk_live_"):
logger.warning(
" API Key envoyée dans Authorization au lieu de X-API-Key"
)
return await self._handle_api_key_auth(
request, token, path, method, call_next
)
logger.debug(f"🎫 JWT détecté pour {method} {path} → délégation à FastAPI")
request.state.authenticated_via = "jwt"
return await call_next(request)
logger.debug(f" Aucune auth pour {method} {path} → délégation à FastAPI")
return await call_next(request)
async def _handle_api_key_auth(
self,
request: Request,
api_key: str,
path: str,
method: str,
call_next: Callable,
):
"""Gère l'authentification par API Key avec vérification STRICTE"""
try:
from database.db_config import async_session_factory
from services.api_key import ApiKeyService
async with async_session_factory() as session:
service = ApiKeyService(session)
api_key_obj = await service.verify_api_key(api_key)
if not api_key_obj:
logger.warning(f" Clé API invalide: {method} {path}")
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={
"detail": "Clé API invalide ou expirée",
"hint": "Vérifiez votre clé X-API-Key",
},
)
is_allowed, rate_info = await service.check_rate_limit(api_key_obj)
if not is_allowed:
logger.warning(f" Rate limit: {api_key_obj.name}")
return JSONResponse(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
content={"detail": "Rate limit dépassé"},
headers={
"X-RateLimit-Limit": str(rate_info["limit"]),
"X-RateLimit-Remaining": "0",
},
)
has_access = await service.check_endpoint_access(api_key_obj, path)
if not has_access:
import json
allowed = (
json.loads(api_key_obj.allowed_endpoints)
if api_key_obj.allowed_endpoints
else ["Tous"]
)
logger.warning(
f" ACCÈS REFUSÉ: {api_key_obj.name}\n"
f" Endpoint demandé: {path}\n"
f" Endpoints autorisés: {allowed}"
)
return JSONResponse(
status_code=status.HTTP_403_FORBIDDEN,
content={
"detail": "Accès non autorisé à cet endpoint",
"endpoint_requested": path,
"api_key_name": api_key_obj.name,
"allowed_endpoints": allowed,
"hint": "Cette clé API n'a pas accès à cet endpoint. Contactez l'administrateur.",
},
)
request.state.api_key = api_key_obj
request.state.authenticated_via = "api_key"
logger.info(f" ACCÈS AUTORISÉ: {api_key_obj.name}{method} {path}")
return await call_next(request)
except Exception as e:
logger.error(f" Erreur validation API Key: {e}", exc_info=True)
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={"detail": f"Erreur interne: {str(e)}"},
)
ApiKeyMiddleware = ApiKeyMiddlewareHTTP
def get_api_key_from_request(request: Request):
"""Récupère l'objet ApiKey depuis la requête si présent""" """Récupère l'objet ApiKey depuis la requête si présent"""
return getattr(request.state, "api_key", None) return getattr(request.state, "api_key", None)
def get_auth_method(request: Request) -> str: def get_auth_method(request: Request) -> str:
"""Retourne la méthode d'authentification utilisée"""
return getattr(request.state, "authenticated_via", "none") return getattr(request.state, "authenticated_via", "none")
__all__ = [
"SwaggerAuthMiddleware",
"ApiKeyMiddlewareHTTP",
"ApiKeyMiddleware",
"get_api_key_from_request",
"get_auth_method",
]

View file

@ -1,66 +1,30 @@
import asyncio
import sys import sys
import os import os
from pathlib import Path from pathlib import Path
_current_file = Path(__file__).resolve() current_dir = Path(__file__).resolve().parent
_script_dir = _current_file.parent parent_dir = current_dir.parent
_app_dir = _script_dir.parent sys.path.insert(0, str(parent_dir))
print(f"DEBUG: Script path: {_current_file}")
print(f"DEBUG: App dir: {_app_dir}")
print(f"DEBUG: Current working dir: {os.getcwd()}")
if str(_app_dir) in sys.path:
sys.path.remove(str(_app_dir))
sys.path.insert(0, str(_app_dir))
os.chdir(str(_app_dir))
print(f"DEBUG: sys.path[0]: {sys.path[0]}")
print(f"DEBUG: New working dir: {os.getcwd()}")
_test_imports = [
"database",
"database.db_config",
"database.models",
"services",
"security",
]
print("\nDEBUG: Vérification des imports...")
for module in _test_imports:
try:
__import__(module)
print(f" {module}")
except ImportError as e:
print(f" {module}: {e}")
import asyncio
import argparse import argparse
import logging
from datetime import datetime from datetime import datetime
import logging
from sqlalchemy import select
try:
from database.db_config import async_session_factory
from database.models.user import User
from database.models.api_key import SwaggerUser, ApiKey
from services.api_key import ApiKeyService
from security.auth import hash_password
except ImportError as e:
print(f"\n ERREUR D'IMPORT: {e}")
print(f" Vérifiez que vous êtes dans /app")
print(f" Commande correcte: cd /app && python scripts/manage_security.py ...")
sys.exit(1)
logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s") logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s")
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from database import get_session
from database.models.api_key import SwaggerUser, ApiKey
from services.api_key import ApiKeyService
from security.auth import hash_password, verify_password
from sqlalchemy import select
async def add_swagger_user(username: str, password: str, full_name: str = None): async def add_swagger_user(username: str, password: str, full_name: str = None):
"""Ajouter un utilisateur Swagger""" """Ajouter un utilisateur Swagger"""
async with async_session_factory() as session:
async for session in get_session():
result = await session.execute( result = await session.execute(
select(SwaggerUser).where(SwaggerUser.username == username) select(SwaggerUser).where(SwaggerUser.username == username)
) )
@ -82,30 +46,39 @@ async def add_swagger_user(username: str, password: str, full_name: str = None):
logger.info(f" Utilisateur Swagger créé: {username}") logger.info(f" Utilisateur Swagger créé: {username}")
logger.info(f" Nom complet: {swagger_user.full_name}") logger.info(f" Nom complet: {swagger_user.full_name}")
logger.info(f" Actif: {swagger_user.is_active}")
break
async def list_swagger_users(): async def list_swagger_users():
"""Lister tous les utilisateurs Swagger""" """Lister tous les utilisateurs Swagger"""
async with async_session_factory() as session:
async for session in get_session():
result = await session.execute(select(SwaggerUser)) result = await session.execute(select(SwaggerUser))
users = result.scalars().all() users = result.scalars().all()
if not users: if not users:
logger.info("🔭 Aucun utilisateur Swagger") logger.info(" Aucun utilisateur Swagger")
return break
logger.info(f" {len(users)} utilisateur(s) Swagger:\n")
logger.info(f"👥 {len(users)} utilisateur(s) Swagger:\n")
for user in users: for user in users:
status = "" if user.is_active else "" status = "" if user.is_active else ""
logger.info(f" {status} {user.username}") logger.info(f" {status} {user.username}")
logger.info(f" Nom: {user.full_name}") logger.info(f" Nom: {user.full_name}")
logger.info(f" Créé: {user.created_at}") logger.info(f" Créé: {user.created_at}")
logger.info(f" Dernière connexion: {user.last_login or 'Jamais'}\n") logger.info(f" Dernière connexion: {user.last_login or 'Jamais'}")
logger.info("")
break
async def delete_swagger_user(username: str): async def delete_swagger_user(username: str):
"""Supprimer un utilisateur Swagger""" """Supprimer un utilisateur Swagger"""
async with async_session_factory() as session:
async for session in get_session():
result = await session.execute( result = await session.execute(
select(SwaggerUser).where(SwaggerUser.username == username) select(SwaggerUser).where(SwaggerUser.username == username)
) )
@ -113,11 +86,13 @@ async def delete_swagger_user(username: str):
if not user: if not user:
logger.error(f" Utilisateur '{username}' introuvable") logger.error(f" Utilisateur '{username}' introuvable")
return break
await session.delete(user) await session.delete(user)
await session.commit() await session.commit()
logger.info(f"🗑️ Utilisateur Swagger supprimé: {username}")
logger.info(f" Utilisateur Swagger supprimé: {username}")
break
async def create_api_key( async def create_api_key(
@ -128,7 +103,8 @@ async def create_api_key(
endpoints: list = None, endpoints: list = None,
): ):
"""Créer une clé API""" """Créer une clé API"""
async with async_session_factory() as session:
async for session in get_session():
service = ApiKeyService(session) service = ApiKeyService(session)
api_key_obj, api_key_plain = await service.create_api_key( api_key_obj, api_key_plain = await service.create_api_key(
@ -140,100 +116,104 @@ async def create_api_key(
allowed_endpoints=endpoints, allowed_endpoints=endpoints,
) )
logger.info("=" * 70) logger.info("=" * 60)
logger.info("🔑 Clé API créée avec succès") logger.info(" Clé API créée avec succès")
logger.info("=" * 70) logger.info("=" * 60)
logger.info(f" ID: {api_key_obj.id}") logger.info(f" ID: {api_key_obj.id}")
logger.info(f" Nom: {api_key_obj.name}") logger.info(f" Nom: {api_key_obj.name}")
logger.info(f" Clé: {api_key_plain}") logger.info(f" Clé: {api_key_plain}")
logger.info(f" Préfixe: {api_key_obj.key_prefix}") logger.info(f" Préfixe: {api_key_obj.key_prefix}")
logger.info(f" Rate limit: {api_key_obj.rate_limit_per_minute} req/min") logger.info(f" Rate limit: {api_key_obj.rate_limit_per_minute} req/min")
logger.info(f" Créée le: {api_key_obj.created_at}")
logger.info(f" Expire le: {api_key_obj.expires_at}") logger.info(f" Expire le: {api_key_obj.expires_at}")
if api_key_obj.allowed_endpoints: if api_key_obj.allowed_endpoints:
import json logger.info(
f" Endpoints autorisés: {', '.join(api_key_obj.allowed_endpoints)}"
try: )
endpoints_list = json.loads(api_key_obj.allowed_endpoints)
logger.info(f" Endpoints: {', '.join(endpoints_list)}")
except:
logger.info(f" Endpoints: {api_key_obj.allowed_endpoints}")
else: else:
logger.info(" Endpoints: Tous (aucune restriction)") logger.info(f" Endpoints autorisés: Tous")
logger.info("=" * 70) logger.info("=" * 60)
logger.info(" SAUVEGARDEZ CETTE CLÉ - Elle ne sera plus affichée !") logger.info(" IMPORTANT: Sauvegardez cette clé, elle ne sera plus affichée !")
logger.info("=" * 70) logger.info("=" * 60)
break
async def list_api_keys(): async def list_api_keys():
"""Lister toutes les clés API""" """Lister toutes les clés API"""
async with async_session_factory() as session:
async for session in get_session():
service = ApiKeyService(session) service = ApiKeyService(session)
keys = await service.list_api_keys() keys = await service.list_api_keys()
if not keys: if not keys:
logger.info("🔭 Aucune clé API") logger.info(" Aucune clé API")
return break
logger.info(f"🔑 {len(keys)} clé(s) API:\n") logger.info(f" {len(keys)} clé(s) API:\n")
for key in keys: for key in keys:
is_valid = key.is_active and ( status = (
not key.expires_at or key.expires_at > datetime.now() ""
if key.is_active
and (not key.expires_at or key.expires_at > datetime.now())
else ""
) )
status = "" if is_valid else ""
logger.info(f" {status} {key.name:<30} ({key.key_prefix}...)") logger.info(f" {status} {key.name:<30} ({key.key_prefix}...)")
logger.info(f" ID: {key.id}") logger.info(f" ID: {key.id}")
logger.info(f" Rate limit: {key.rate_limit_per_minute} req/min") logger.info(f" Rate limit: {key.rate_limit_per_minute} req/min")
logger.info(f" Requêtes: {key.total_requests}") logger.info(f" Requêtes: {key.total_requests}")
logger.info(f" Expire: {key.expires_at or 'Jamais'}") logger.info(f" Créée le: {key.created_at}")
logger.info(f" Expire le: {key.expires_at or 'Jamais'}")
logger.info(f" Dernière utilisation: {key.last_used_at or 'Jamais'}") logger.info(f" Dernière utilisation: {key.last_used_at or 'Jamais'}")
if key.allowed_endpoints: if key.allowed_endpoints:
import json logger.info(
f" Endpoints: {', '.join(key.allowed_endpoints[:3])}..."
)
try:
endpoints = json.loads(key.allowed_endpoints)
display = ", ".join(endpoints[:4])
if len(endpoints) > 4:
display += f"... (+{len(endpoints) - 4})"
logger.info(f" Endpoints: {display}")
except:
pass
else:
logger.info(" Endpoints: Tous")
logger.info("") logger.info("")
break
async def revoke_api_key(key_id: str): async def revoke_api_key(key_id: str):
"""Révoquer une clé API""" """Révoquer une clé API"""
async with async_session_factory() as session:
async for session in get_session():
service = ApiKeyService(session)
result = await session.execute(select(ApiKey).where(ApiKey.id == key_id)) result = await session.execute(select(ApiKey).where(ApiKey.id == key_id))
key = result.scalar_one_or_none() key = result.scalar_one_or_none()
if not key: if not key:
logger.error(f" Clé API '{key_id}' introuvable") logger.error(f" Clé API '{key_id}' introuvable")
return break
key.is_active = False key.is_active = False
key.revoked_at = datetime.now()
await session.commit() await session.commit()
logger.info(f"🗑️ Clé API révoquée: {key.name}") logger.info(f" Clé API révoquée: {key.name}")
logger.info(f" ID: {key.id}") logger.info(f" ID: {key.id}")
logger.info(f" Préfixe: {key.key_prefix}")
break
async def verify_api_key(api_key: str): async def verify_api_key(api_key: str):
"""Vérifier une clé API""" """Vérifier une clé API"""
async with async_session_factory() as session:
async for session in get_session():
service = ApiKeyService(session) service = ApiKeyService(session)
key = await service.verify_api_key(api_key) key = await service.verify_api_key(api_key)
if not key: if not key:
logger.error(" Clé API invalide ou expirée") logger.error(" Clé API invalide ou expirée")
return break
logger.info("=" * 60) logger.info("=" * 60)
logger.info(" Clé API valide") logger.info(" Clé API valide")
@ -242,67 +222,61 @@ async def verify_api_key(api_key: str):
logger.info(f" ID: {key.id}") logger.info(f" ID: {key.id}")
logger.info(f" Rate limit: {key.rate_limit_per_minute} req/min") logger.info(f" Rate limit: {key.rate_limit_per_minute} req/min")
logger.info(f" Requêtes totales: {key.total_requests}") logger.info(f" Requêtes totales: {key.total_requests}")
logger.info(f" Expire: {key.expires_at or 'Jamais'}") logger.info(f" Expire le: {key.expires_at or 'Jamais'}")
logger.info(f" Dernière utilisation: {key.last_used_at or 'Jamais'}")
if key.allowed_endpoints:
import json
try:
endpoints = json.loads(key.allowed_endpoints)
logger.info(f" Endpoints autorisés: {endpoints}")
except:
pass
else:
logger.info(" Endpoints autorisés: Tous")
logger.info("=" * 60) logger.info("=" * 60)
break
async def main(): async def main():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Gestion des utilisateurs Swagger et clés API", description="Gestion des utilisateurs Swagger et clés API"
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Exemples:
python scripts/manage_security.py swagger add admin MyP@ssw0rd
python scripts/manage_security.py swagger list
python scripts/manage_security.py apikey create "Mon App" --days 365 --rate-limit 100
python scripts/manage_security.py apikey create "SDK-ReadOnly" --endpoints "/clients" "/clients/*" "/devis" "/devis/*"
python scripts/manage_security.py apikey list
python scripts/manage_security.py apikey verify sdk_live_xxxxx
""",
) )
subparsers = parser.add_subparsers(dest="command", help="Commandes") subparsers = parser.add_subparsers(dest="command", help="Commandes disponibles")
swagger_parser = subparsers.add_parser("swagger", help="Gestion Swagger") swagger_parser = subparsers.add_parser(
swagger_sub = swagger_parser.add_subparsers(dest="swagger_command") "swagger", help="Gestion des utilisateurs Swagger"
)
swagger_subparsers = swagger_parser.add_subparsers(dest="swagger_command")
add_p = swagger_sub.add_parser("add", help="Ajouter utilisateur") add_parser = swagger_subparsers.add_parser("add", help="Ajouter un utilisateur")
add_p.add_argument("username", help="Nom d'utilisateur") add_parser.add_argument("username", help="Nom d'utilisateur")
add_p.add_argument("password", help="Mot de passe") add_parser.add_argument("password", help="Mot de passe")
add_p.add_argument("--full-name", help="Nom complet") add_parser.add_argument("--full-name", help="Nom complet (optionnel)")
swagger_sub.add_parser("list", help="Lister utilisateurs") swagger_subparsers.add_parser("list", help="Lister les utilisateurs")
del_p = swagger_sub.add_parser("delete", help="Supprimer utilisateur") delete_parser = swagger_subparsers.add_parser(
del_p.add_argument("username", help="Nom d'utilisateur") "delete", help="Supprimer un utilisateur"
)
delete_parser.add_argument("username", help="Nom d'utilisateur")
apikey_parser = subparsers.add_parser("apikey", help="Gestion clés API") apikey_parser = subparsers.add_parser("apikey", help="Gestion des clés API")
apikey_sub = apikey_parser.add_subparsers(dest="apikey_command") apikey_subparsers = apikey_parser.add_subparsers(dest="apikey_command")
create_p = apikey_sub.add_parser("create", help="Créer clé API") create_parser = apikey_subparsers.add_parser("create", help="Créer une clé API")
create_p.add_argument("name", help="Nom de la clé") create_parser.add_argument("name", help="Nom de la clé")
create_p.add_argument("--description", help="Description") create_parser.add_argument("--description", help="Description (optionnel)")
create_p.add_argument("--days", type=int, default=365, help="Expiration (jours)") create_parser.add_argument(
create_p.add_argument("--rate-limit", type=int, default=60, help="Req/min") "--days", type=int, default=365, help="Jours avant expiration (défaut: 365)"
create_p.add_argument("--endpoints", nargs="+", help="Endpoints autorisés") )
create_parser.add_argument(
"--rate-limit", type=int, default=60, help="Requêtes par minute (défaut: 60)"
)
create_parser.add_argument(
"--endpoints",
nargs="+",
help="Endpoints autorisés (ex: /clients /articles)",
)
apikey_sub.add_parser("list", help="Lister clés") apikey_subparsers.add_parser("list", help="Lister les clés API")
rev_p = apikey_sub.add_parser("revoke", help="Révoquer clé") revoke_parser = apikey_subparsers.add_parser("revoke", help="Révoquer une clé")
rev_p.add_argument("key_id", help="ID de la clé") revoke_parser.add_argument("key_id", help="ID de la clé")
ver_p = apikey_sub.add_parser("verify", help="Vérifier clé") verify_parser = apikey_subparsers.add_parser("verify", help="Vérifier une clé")
ver_p.add_argument("api_key", help="Clé API complète") verify_parser.add_argument("api_key", help="Clé API complète")
args = parser.parse_args() args = parser.parse_args()
@ -343,7 +317,7 @@ if __name__ == "__main__":
try: try:
asyncio.run(main()) asyncio.run(main())
except KeyboardInterrupt: except KeyboardInterrupt:
print("\n Interrupted") logger.info("\n Interrupted")
sys.exit(0) sys.exit(0)
except Exception as e: except Exception as e:
logger.error(f" Erreur: {e}") logger.error(f" Erreur: {e}")

View file

@ -5,12 +5,10 @@ import jwt
import secrets import secrets
import hashlib import hashlib
from config.config import settings SECRET_KEY = "VOTRE_SECRET_KEY_A_METTRE_EN_.ENV"
ALGORITHM = "HS256"
SECRET_KEY = settings.jwt_secret ACCESS_TOKEN_EXPIRE_MINUTES = 10080
ALGORITHM = settings.jwt_algorithm REFRESH_TOKEN_EXPIRE_DAYS = 7
ACCESS_TOKEN_EXPIRE_MINUTES = settings.access_token_expire_minutes
REFRESH_TOKEN_EXPIRE_DAYS = settings.refresh_token_expire_days
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
@ -69,13 +67,9 @@ def decode_token(token: str) -> Optional[Dict]:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload return payload
except jwt.ExpiredSignatureError: except jwt.ExpiredSignatureError:
raise jwt.InvalidTokenError("Token expiré") return None
except jwt.DecodeError: except jwt.JWTError:
raise jwt.InvalidTokenError("Token invalide (format incorrect)") return None
except jwt.InvalidTokenError as e:
raise jwt.InvalidTokenError(f"Token invalide: {str(e)}")
except Exception as e:
raise jwt.InvalidTokenError(f"Erreur lors du décodage du token: {str(e)}")
def validate_password_strength(password: str) -> tuple[bool, str]: def validate_password_strength(password: str) -> tuple[bool, str]:

View file

@ -134,7 +134,7 @@ class ApiKeyService:
api_key_obj.revoked_at = datetime.now() api_key_obj.revoked_at = datetime.now()
await self.session.commit() await self.session.commit()
logger.info(f"🗑️ Clé API révoquée: {api_key_obj.name}") logger.info(f" Clé API révoquée: {api_key_obj.name}")
return True return True
async def get_by_id(self, key_id: str) -> Optional[ApiKey]: async def get_by_id(self, key_id: str) -> Optional[ApiKey]:
@ -150,42 +150,24 @@ class ApiKeyService:
} }
async def check_endpoint_access(self, api_key_obj: ApiKey, endpoint: str) -> bool: async def check_endpoint_access(self, api_key_obj: ApiKey, endpoint: str) -> bool:
"""Vérifie si la clé a accès à un endpoint spécifique"""
if not api_key_obj.allowed_endpoints: if not api_key_obj.allowed_endpoints:
logger.debug(
f"🔓 API Key {api_key_obj.name}: Aucune restriction d'endpoint"
)
return True return True
try: try:
allowed = json.loads(api_key_obj.allowed_endpoints) allowed = json.loads(api_key_obj.allowed_endpoints)
if "*" in allowed or "/*" in allowed:
logger.debug(f"🔓 API Key {api_key_obj.name}: Accès global autorisé")
return True
for pattern in allowed: for pattern in allowed:
if pattern == "*":
return True
if pattern.endswith("*"):
prefix = pattern[:-1]
if endpoint.startswith(prefix):
return True
if pattern == endpoint: if pattern == endpoint:
logger.debug(f" Match exact: {pattern} == {endpoint}")
return True return True
if pattern.endswith("/*"):
base = pattern[:-2] # "/clients/*" → "/clients"
if endpoint == base or endpoint.startswith(base + "/"):
logger.debug(f" Match wildcard: {pattern}{endpoint}")
return True
elif pattern.endswith("*"):
base = pattern[:-1] # "/clients*" → "/clients"
if endpoint.startswith(base):
logger.debug(f" Match prefix: {pattern}{endpoint}")
return True
logger.warning(
f" API Key {api_key_obj.name}: Accès refusé à {endpoint}\n"
f" Endpoints autorisés: {allowed}"
)
return False return False
except json.JSONDecodeError: except json.JSONDecodeError:
logger.error(f" Erreur parsing allowed_endpoints pour {api_key_obj.id}") logger.error(f" Erreur parsing allowed_endpoints pour {api_key_obj.id}")
return False return False