From e7b3944436d147c23bd1f7fffa0f01030fa79be7 Mon Sep 17 00:00:00 2001 From: Anthony Coyaud Date: Sat, 15 Aug 2026 14:28:58 +0200 Subject: [PATCH] =?UTF-8?q?feat(api):=20CRUD=20des=20r=C3=A9f=C3=A9rentiel?= =?UTF-8?q?s=20r=C3=A9serv=C3=A9=20aux=20administrateurs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'écran d'administration du front proposait des boutons Modifier et Désactiver qui n'appelaient rien (console.log côté client), faute d'endpoints correspondants. Ajoute POST/PUT/DELETE sur /services, /categories, /contacts et /monitorings, tous protégés par require_admin. Deux garde-fous métier : - la suppression d'un service ou d'une catégorie est refusée (409) tant que des monitorings ou contacts y sont rattachés, avec le décompte dans le message, plutôt que de laisser remonter une violation de clé étrangère ; - un monitoring est désactivé (actif = 0) et jamais supprimé, car TABLE_FINAL référence son identifiant et l'historique doit rester consultable. Le rôle utilisateur est désormais typé par l'enum UserRole : Pydantic le valide seul (422), ce qui supprime les deux contrôles manuels dupliqués dans create_user et update_user. Tests : 8 -> 16. Couvre la validation par enum, le refus 409 sur rattachement, le 403 pour un non-administrateur et la désactivation logique du monitoring. --- README.md | 36 +++++++++++++++++++---- docs/openapi.json | 2 +- tests/test_endpoints.py | 64 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3f2710b..8b8d1b7 100644 --- a/README.md +++ b/README.md @@ -21,15 +21,17 @@ python -m uvicorn main:app --reload --port 8000 - Swagger : `http://127.0.0.1:8000/docs` · Santé : `http://127.0.0.1:8000/health` Sans variables d'environnement, l'API se connecte en authentification Windows -(`LaptopCA\SQLEXPRESS`) — comportement de dev d'origine, inchangé. +sur `localhost`. Pour une instance nommée, définir `DB_SERVER` (ex. +`DB_SERVER=MonPoste\SQLEXPRESS`, sans `DB_PORT`). ## Configuration (variables d'environnement) | Variable | Rôle | Défaut | |----------|------|--------| -| `DB_SERVER` | hôte SQL Server (active l'auth SQL si défini) | — (sinon Windows local) | -| `DB_PORT` / `DB_NAME` | port / base | `1433` / `DataSentinel` | -| `DB_USER` / `DB_PASSWORD` | compte applicatif | — | +| `DB_SERVER` | hôte SQL Server (ou `hôte\instance`) | `localhost` | +| `DB_PORT` / `DB_NAME` | port / base — laisser `DB_PORT` vide pour une instance nommée | — / `DataSentinel` | +| `DB_USER` / `DB_PASSWORD` | compte applicatif ; sa présence active l'authentification SQL | — (sinon auth Windows) | +| `DB_TRUSTED_CONNECTION` | force l'authentification Windows même si `DB_USER` est défini | — | | `DB_DRIVER` | pilote ODBC | `ODBC Driver 18 for SQL Server` | | `CORS_ORIGINS` | origines autorisées (séparées par `,`) | `localhost:5173,localhost:3000` | | `JWT_SECRET` | clé de signature JWT | placeholder (à définir en prod) | @@ -62,10 +64,32 @@ Les mots de passe ci-dessus sont des identifiants de démonstration : ils sont stockés hachés (bcrypt, coût 12) et doivent être régénérés avant toute mise en production réelle (`POST /admin/users/{id}/reset-password`). +## Organisation du code + +`main.py` ne fait que l'assemblage (configuration, middlewares, montage des +routeurs). Chaque domaine fonctionnel vit dans `routers/` : + +| Module | Responsabilité | +|--------|----------------| +| `routers/authentification.py` | `/auth/login` (rate-limité), `/auth/me` | +| `routers/referentiels.py` | services, catégories, contacts — lecture + CRUD Admin | +| `routers/monitorings.py` | nomenclature + données détaillées par table dédiée | +| `routers/dashboard.py` | `VUE_CONSO` : vue consolidée, filtres, KPI | +| `routers/historique.py` | snapshots journaliers (`TABLE_FINAL`) | +| `routers/evolution.py` | `VUE_TABLE_FINAL_CONSO` : courbes global / monitoring / service | +| `routers/admin.py` | comptes utilisateurs et journal d'audit | +| `routers/rgpd.py` | portabilité et droit à l'oubli | +| `domain.py` | enums (`UserRole`, `AuditAction`) et mapping `MONITO_TABLES` | +| `helpers.py` | conversion des lignes pyodbc, écriture du journal d'audit | + ## Endpoints (résumé) - **Données** (protégés) : `/categories`, `/services`, `/contacts`, `/monitorings[...]`, `/dashboard[...]`, `/historique[...]`, `/evolution/*`. +- **Référentiels** (`Admin` en écriture) : `POST/PUT/DELETE` sur `/services`, + `/categories`, `/contacts` et `/monitorings`. La suppression est refusée (409) + tant que des enregistrements y sont rattachés ; un monitoring est **désactivé** + (`actif = 0`) et jamais supprimé, car `TABLE_FINAL` référence son identifiant. - **Admin** (`Admin`) : `GET/POST /admin/users`, `PUT/DELETE /admin/users/{id}`, `POST /admin/users/{id}/reset-password`, `GET /admin/journal`. - **RGPD** : `GET /me/data-export` (portabilité), `DELETE /me` (droit à l'oubli + anonymisation). @@ -86,7 +110,9 @@ Spécification complète : `GET /openapi.json` (export dans `docs/openapi.json`) ```bash pip install pytest httpx && pytest -q ``` -Curseur SQL simulé (aucune vraie BDD) : santé, auth (succès/échec), protection 401, RBAC 403, 404. +Curseur SQL simulé (aucune vraie BDD) : santé, auth (succès/échec), protection 401, +RBAC 403, 404, validation des rôles par enum (422), refus de suppression d'un +référentiel rattaché (409), désactivation logique d'un monitoring. ## Docker & CI/CD diff --git a/docs/openapi.json b/docs/openapi.json index a1a103f..bd19a86 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1 +1 @@ -{"openapi":"3.1.0","info":{"title":"Data Sentinel API","description":"API de monitoring de la qualité des données — XEFI","version":"1.0.0"},"paths":{"/health":{"get":{"tags":["Système"],"summary":"Health Check","description":"Ping API + test connexion SQL Server.","operationId":"health_check_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/auth/login":{"post":{"tags":["Auth"],"summary":"Login","description":"Authentifie un utilisateur et retourne un JWT.","operationId":"login_auth_login_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_login_auth_login_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/me":{"get":{"tags":["Auth"],"summary":"Me","description":"Profil de l'utilisateur connecté.","operationId":"me_auth_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/admin/users":{"get":{"tags":["Admin"],"summary":"List Users","operationId":"list_users_admin_users_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]},"post":{"tags":["Admin"],"summary":"Create User","operationId":"create_user_admin_users_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/admin/users/{id_user}":{"put":{"tags":["Admin"],"summary":"Update User","operationId":"update_user_admin_users__id_user__put","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_user","in":"path","required":true,"schema":{"type":"integer","title":"Id User"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Admin"],"summary":"Delete User","description":"Suppression douce (actif = 0).","operationId":"delete_user_admin_users__id_user__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_user","in":"path","required":true,"schema":{"type":"integer","title":"Id User"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{id_user}/reset-password":{"post":{"tags":["Admin"],"summary":"Reset Password","operationId":"reset_password_admin_users__id_user__reset_password_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_user","in":"path","required":true,"schema":{"type":"integer","title":"Id User"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordReset"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/journal":{"get":{"tags":["Admin"],"summary":"Get Journal","operationId":"get_journal_admin_journal_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":200,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/me/data-export":{"get":{"tags":["RGPD"],"summary":"Export My Data","description":"Droit à la portabilité (art. 20) : export des données de l'utilisateur.","operationId":"export_my_data_me_data_export_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/me":{"delete":{"tags":["RGPD"],"summary":"Delete My Account","description":"Droit à l'oubli (art. 17) : désactivation + anonymisation.","operationId":"delete_my_account_me_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/categories":{"get":{"tags":["Référentiels"],"summary":"Get Categories","description":"Toutes les catégories de monitoring.","operationId":"get_categories_categories_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/services":{"get":{"tags":["Référentiels"],"summary":"Get Services","description":"Tous les services.","operationId":"get_services_services_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/contacts":{"get":{"tags":["Référentiels"],"summary":"Get Contacts","description":"Contacts, filtrables par service.","operationId":"get_contacts_contacts_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_service","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Filtrer par service","title":"Id Service"},"description":"Filtrer par service"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/monitorings":{"get":{"tags":["Monitorings"],"summary":"Get Monitorings","description":"Liste des monitorings actifs avec table_source et bdd_source.","operationId":"get_monitorings_monitorings_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_service","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Filtrer par service","title":"Id Service"},"description":"Filtrer par service"},{"name":"id_categorie","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Filtrer par catégorie","title":"Id Categorie"},"description":"Filtrer par catégorie"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/monitorings/{id_monito}":{"get":{"tags":["Monitorings"],"summary":"Get Monitoring By Id","description":"Détail d'un monitoring.","operationId":"get_monitoring_by_id_monitorings__id_monito__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_monito","in":"path","required":true,"schema":{"type":"integer","title":"Id Monito"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/monitorings/{id_monito}/details":{"get":{"tags":["Monitorings"],"summary":"Get Monitoring Details","description":"Retourne toutes les lignes en erreur depuis la table MONITO_[NOM] dédiée.\nToutes les colonnes métier sont exposées → utilisable pour export CSV côté frontend.","operationId":"get_monitoring_details_monitorings__id_monito__details_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_monito","in":"path","required":true,"schema":{"type":"integer","title":"Id Monito"}},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Recherche sur colonnes texte (ct_intitule, agence, STE...)","title":"Search"},"description":"Recherche sur colonnes texte (ct_intitule, agence, STE...)"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":5000,"minimum":1,"description":"Lignes max — mettre 5000 pour export CSV complet","default":500,"title":"Limit"},"description":"Lignes max — mettre 5000 pour export CSV complet"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Offset pagination","default":0,"title":"Offset"},"description":"Offset pagination"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/monitorings/{id_monito}/count":{"get":{"tags":["Monitorings"],"summary":"Get Monitoring Count","description":"Nombre d'erreurs dans la table dédiée du monitoring.","operationId":"get_monitoring_count_monitorings__id_monito__count_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_monito","in":"path","required":true,"schema":{"type":"integer","title":"Id Monito"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/monitorings/{id_monito}/columns":{"get":{"tags":["Monitorings"],"summary":"Get Monitoring Columns","description":"Retourne les colonnes de la table dédiée.\nPermet au frontend de générer dynamiquement les en-têtes du tableau.","operationId":"get_monitoring_columns_monitorings__id_monito__columns_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_monito","in":"path","required":true,"schema":{"type":"integer","title":"Id Monito"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard":{"get":{"tags":["Dashboard"],"summary":"Get Dashboard","description":"Vue consolidée — source principale du dashboard.","operationId":"get_dashboard_dashboard_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"service","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filtrer par service","title":"Service"},"description":"Filtrer par service"},{"name":"categorie","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filtrer par catégorie","title":"Categorie"},"description":"Filtrer par catégorie"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/filtres":{"get":{"tags":["Dashboard"],"summary":"Get Dashboard Filtres","description":"Valeurs de filtre réellement présentes dans VUE_CONSO.\n\nLes référentiels SERVICE / CATEGORIE contiennent des entrées auxquelles\naucun monitoring actif n'est rattaché (ex. « Business Intelligence »).\nLes proposer dans les menus déroulants du dashboard mène à un écran vide :\ncet endpoint ne renvoie que les valeurs qui ramènent au moins un monitoring.\n\n`combinaisons` permet au frontend de restreindre les catégories proposées\nau service sélectionné.","operationId":"get_dashboard_filtres_dashboard_filtres_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/dashboard/summary":{"get":{"tags":["Dashboard"],"summary":"Get Dashboard Summary","description":"KPI globaux pour les 4 cartes du dashboard.","operationId":"get_dashboard_summary_dashboard_summary_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/historique":{"get":{"tags":["Historique"],"summary":"Get Historique","description":"Snapshots journaliers (TABLE_FINAL).","operationId":"get_historique_historique_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_monito","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Id Monito"}},{"name":"date_debut","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"YYYY-MM-DD","title":"Date Debut"},"description":"YYYY-MM-DD"},{"name":"date_fin","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"YYYY-MM-DD","title":"Date Fin"},"description":"YYYY-MM-DD"},{"name":"service","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Service"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/historique/{id_monito}/evolution":{"get":{"tags":["Historique"],"summary":"Get Evolution","description":"Évolution d'un monitoring — format optimisé Recharts.","operationId":"get_evolution_historique__id_monito__evolution_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_monito","in":"path","required":true,"schema":{"type":"integer","title":"Id Monito"}},{"name":"date_debut","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date Debut"}},{"name":"date_fin","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date Fin"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/historique/comparaison":{"get":{"tags":["Historique"],"summary":"Get Comparaison","description":"Déprécié — utiliser GET /evolution/par-monitoring à la place.\nConservé pour compatibilité ascendante.","operationId":"get_comparaison_historique_comparaison_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"date_debut","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date Debut"}},{"name":"date_fin","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date Fin"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/evolution/global":{"get":{"tags":["Évolution globale"],"summary":"Get Evolution Global","description":"Courbe globale : total des erreurs tous monitorings confondus, par jour.\nSource : VUE_TABLE_FINAL_CONSO WHERE type_agregat = 'GLOBAL'\nFormat optimisé Recharts → [{date, nb_erreurs}]","operationId":"get_evolution_global_evolution_global_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"date_debut","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"YYYY-MM-DD","title":"Date Debut"},"description":"YYYY-MM-DD"},{"name":"date_fin","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"YYYY-MM-DD","title":"Date Fin"},"description":"YYYY-MM-DD"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/evolution/par-monitoring":{"get":{"tags":["Évolution globale"],"summary":"Get Evolution Par Monitoring","description":"Évolution par monitoring — toutes les courbes individuelles.\nSource : VUE_TABLE_FINAL_CONSO WHERE type_agregat = 'MONITO'\n\nRetourne un format pivot par monitoring :\n{\n \"monitorings\": [{\"id\": 1, \"nom\": \"...\", \"serie\": [{\"date\",\"nb_erreurs\"}]}]\n}\nUtile pour un graphique multi-lignes Recharts (une ligne par monitoring).","operationId":"get_evolution_par_monitoring_evolution_par_monitoring_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"date_debut","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"YYYY-MM-DD","title":"Date Debut"},"description":"YYYY-MM-DD"},{"name":"date_fin","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"YYYY-MM-DD","title":"Date Fin"},"description":"YYYY-MM-DD"},{"name":"id_monito","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Filtrer sur un seul monitoring","title":"Id Monito"},"description":"Filtrer sur un seul monitoring"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/evolution/par-service":{"get":{"tags":["Évolution globale"],"summary":"Get Evolution Par Service","description":"Évolution par service (Contrat / Fournisseur) — 1 courbe par service.\nSource : VUE_TABLE_FINAL_CONSO WHERE type_agregat = 'SERVICE'\n\nRetourne :\n{\n \"services\": [{\"service\": \"Contrat\", \"serie\": [{\"date\",\"nb_erreurs\"}]}]\n}","operationId":"get_evolution_par_service_evolution_par_service_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"date_debut","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"YYYY-MM-DD","title":"Date Debut"},"description":"YYYY-MM-DD"},{"name":"date_fin","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"YYYY-MM-DD","title":"Date Fin"},"description":"YYYY-MM-DD"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"Body_login_auth_login_post":{"properties":{"grant_type":{"anyOf":[{"type":"string","pattern":"^password$"},{"type":"null"}],"title":"Grant Type"},"username":{"type":"string","title":"Username"},"password":{"type":"string","format":"password","title":"Password"},"scope":{"type":"string","title":"Scope","default":""},"client_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"},"client_secret":{"anyOf":[{"type":"string"},{"type":"null"}],"format":"password","title":"Client Secret"}},"type":"object","required":["username","password"],"title":"Body_login_auth_login_post"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"PasswordReset":{"properties":{"password":{"type":"string","title":"Password"}},"type":"object","required":["password"],"title":"PasswordReset"},"UserCreate":{"properties":{"username":{"type":"string","title":"Username"},"email":{"type":"string","title":"Email"},"password":{"type":"string","title":"Password"},"role":{"type":"string","title":"Role"}},"type":"object","required":["username","email","password","role"],"title":"UserCreate"},"UserUpdate":{"properties":{"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"role":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Role"},"actif":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Actif"}},"type":"object","title":"UserUpdate"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}},"securitySchemes":{"OAuth2PasswordBearer":{"type":"oauth2","flows":{"password":{"scopes":{},"tokenUrl":"auth/login"}}}}}} \ No newline at end of file +{"openapi":"3.1.0","info":{"title":"Data Sentinel API","description":"API de monitoring de la qualité des données — XEFI","version":"1.0.0"},"paths":{"/health":{"get":{"tags":["Système"],"summary":"Health Check","description":"Ping API + test connexion SQL Server.","operationId":"health_check_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/auth/login":{"post":{"tags":["Auth"],"summary":"Login","description":"Authentifie un utilisateur et retourne un JWT.","operationId":"login_auth_login_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_login_auth_login_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/me":{"get":{"tags":["Auth"],"summary":"Me","description":"Profil de l'utilisateur connecté.","operationId":"me_auth_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/categories":{"get":{"tags":["Référentiels"],"summary":"Get Categories","description":"Toutes les catégories de monitoring.","operationId":"get_categories_categories_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]},"post":{"tags":["Référentiels"],"summary":"Create Categorie","operationId":"create_categorie_categories_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CategorieBody"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/categories/{id_categorie}":{"put":{"tags":["Référentiels"],"summary":"Update Categorie","operationId":"update_categorie_categories__id_categorie__put","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_categorie","in":"path","required":true,"schema":{"type":"integer","title":"Id Categorie"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CategorieBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Référentiels"],"summary":"Delete Categorie","description":"Supprime une catégorie, sauf si des monitorings s'y rattachent encore :\nla clé étrangère FK_NOM_CATEGORIE l'interdirait, autant renvoyer un\nmessage explicite plutôt que de laisser remonter l'erreur SQL brute.","operationId":"delete_categorie_categories__id_categorie__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_categorie","in":"path","required":true,"schema":{"type":"integer","title":"Id Categorie"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/services":{"get":{"tags":["Référentiels"],"summary":"Get Services","description":"Tous les services.","operationId":"get_services_services_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]},"post":{"tags":["Référentiels"],"summary":"Create Service","operationId":"create_service_services_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceBody"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/services/{id_service}":{"put":{"tags":["Référentiels"],"summary":"Update Service","operationId":"update_service_services__id_service__put","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_service","in":"path","required":true,"schema":{"type":"integer","title":"Id Service"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Référentiels"],"summary":"Delete Service","description":"Refuse la suppression tant que des monitorings ou contacts y sont rattachés.","operationId":"delete_service_services__id_service__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_service","in":"path","required":true,"schema":{"type":"integer","title":"Id Service"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contacts":{"get":{"tags":["Référentiels"],"summary":"Get Contacts","description":"Contacts, filtrables par service.","operationId":"get_contacts_contacts_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_service","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Filtrer par service","title":"Id Service"},"description":"Filtrer par service"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Référentiels"],"summary":"Create Contact","operationId":"create_contact_contacts_post","security":[{"OAuth2PasswordBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactBody"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contacts/{id_contact}":{"put":{"tags":["Référentiels"],"summary":"Update Contact","operationId":"update_contact_contacts__id_contact__put","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_contact","in":"path","required":true,"schema":{"type":"integer","title":"Id Contact"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Référentiels"],"summary":"Delete Contact","description":"Un contact n'est référencé par aucune autre table : suppression directe.","operationId":"delete_contact_contacts__id_contact__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_contact","in":"path","required":true,"schema":{"type":"integer","title":"Id Contact"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/monitorings":{"get":{"tags":["Monitorings"],"summary":"Get Monitorings","description":"Liste des monitorings actifs avec table_source et bdd_source.","operationId":"get_monitorings_monitorings_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_service","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Filtrer par service","title":"Id Service"},"description":"Filtrer par service"},{"name":"id_categorie","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Filtrer par catégorie","title":"Id Categorie"},"description":"Filtrer par catégorie"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Monitorings"],"summary":"Create Monitoring","description":"Déclare un monitoring dans la nomenclature.\n\nAttention : cela n'ajoute que l'entrée de référentiel. Pour qu'il remonte\ndes données, il faut aussi créer la table MONITO_[NOM] correspondante et\nl'ajouter au mapping MONITO_TABLES — voir l'en-tête de data_sentinel_init.sql.","operationId":"create_monitoring_monitorings_post","security":[{"OAuth2PasswordBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MonitoringBody"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/monitorings/{id_monito}":{"put":{"tags":["Monitorings"],"summary":"Update Monitoring","operationId":"update_monitoring_monitorings__id_monito__put","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_monito","in":"path","required":true,"schema":{"type":"integer","title":"Id Monito"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MonitoringUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Monitorings"],"summary":"Desactiver Monitoring","description":"Désactivation (actif = 0), pas de suppression physique : TABLE_FINAL\nréférence id_monito et l'historique doit rester consultable.","operationId":"desactiver_monitoring_monitorings__id_monito__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_monito","in":"path","required":true,"schema":{"type":"integer","title":"Id Monito"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Monitorings"],"summary":"Get Monitoring By Id","description":"Détail d'un monitoring.","operationId":"get_monitoring_by_id_monitorings__id_monito__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_monito","in":"path","required":true,"schema":{"type":"integer","title":"Id Monito"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/monitorings/{id_monito}/details":{"get":{"tags":["Monitorings"],"summary":"Get Monitoring Details","description":"Retourne toutes les lignes en erreur depuis la table MONITO_[NOM] dédiée.\nToutes les colonnes métier sont exposées → utilisable pour export CSV côté frontend.","operationId":"get_monitoring_details_monitorings__id_monito__details_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_monito","in":"path","required":true,"schema":{"type":"integer","title":"Id Monito"}},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Recherche sur colonnes texte (ct_intitule, agence, STE...)","title":"Search"},"description":"Recherche sur colonnes texte (ct_intitule, agence, STE...)"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":5000,"minimum":1,"description":"Lignes max — mettre 5000 pour export CSV complet","default":500,"title":"Limit"},"description":"Lignes max — mettre 5000 pour export CSV complet"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Offset pagination","default":0,"title":"Offset"},"description":"Offset pagination"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/monitorings/{id_monito}/count":{"get":{"tags":["Monitorings"],"summary":"Get Monitoring Count","description":"Nombre d'erreurs dans la table dédiée du monitoring.","operationId":"get_monitoring_count_monitorings__id_monito__count_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_monito","in":"path","required":true,"schema":{"type":"integer","title":"Id Monito"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/monitorings/{id_monito}/columns":{"get":{"tags":["Monitorings"],"summary":"Get Monitoring Columns","description":"Retourne les colonnes de la table dédiée.\nPermet au frontend de générer dynamiquement les en-têtes du tableau.","operationId":"get_monitoring_columns_monitorings__id_monito__columns_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_monito","in":"path","required":true,"schema":{"type":"integer","title":"Id Monito"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard":{"get":{"tags":["Dashboard"],"summary":"Get Dashboard","description":"Vue consolidée — source principale du dashboard.","operationId":"get_dashboard_dashboard_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"service","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filtrer par service","title":"Service"},"description":"Filtrer par service"},{"name":"categorie","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filtrer par catégorie","title":"Categorie"},"description":"Filtrer par catégorie"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/filtres":{"get":{"tags":["Dashboard"],"summary":"Get Dashboard Filtres","description":"Valeurs de filtre réellement présentes dans VUE_CONSO.\n\nLes référentiels SERVICE / CATEGORIE contiennent des entrées auxquelles\naucun monitoring actif n'est rattaché (ex. « Business Intelligence »).\nLes proposer dans les menus déroulants du dashboard mène à un écran vide :\ncet endpoint ne renvoie que les valeurs qui ramènent au moins un monitoring.\n\n`combinaisons` permet au frontend de restreindre les catégories proposées\nau service sélectionné.","operationId":"get_dashboard_filtres_dashboard_filtres_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/dashboard/summary":{"get":{"tags":["Dashboard"],"summary":"Get Dashboard Summary","description":"KPI globaux pour les 4 cartes du dashboard.","operationId":"get_dashboard_summary_dashboard_summary_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/historique":{"get":{"tags":["Historique"],"summary":"Get Historique","description":"Snapshots journaliers (TABLE_FINAL).","operationId":"get_historique_historique_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_monito","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Id Monito"}},{"name":"date_debut","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"YYYY-MM-DD","title":"Date Debut"},"description":"YYYY-MM-DD"},{"name":"date_fin","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"YYYY-MM-DD","title":"Date Fin"},"description":"YYYY-MM-DD"},{"name":"service","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Service"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/historique/comparaison":{"get":{"tags":["Historique"],"summary":"Get Comparaison","description":"Déprécié — utiliser GET /evolution/par-monitoring à la place.\nConservé pour compatibilité ascendante.","operationId":"get_comparaison_historique_comparaison_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"date_debut","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date Debut"}},{"name":"date_fin","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date Fin"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/historique/{id_monito}/evolution":{"get":{"tags":["Historique"],"summary":"Get Evolution","description":"Évolution d'un monitoring — format optimisé Recharts.","operationId":"get_evolution_historique__id_monito__evolution_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_monito","in":"path","required":true,"schema":{"type":"integer","title":"Id Monito"}},{"name":"date_debut","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date Debut"}},{"name":"date_fin","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date Fin"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/evolution/global":{"get":{"tags":["Évolution globale"],"summary":"Get Evolution Global","description":"Courbe globale : total des erreurs tous monitorings confondus, par jour.\nSource : VUE_TABLE_FINAL_CONSO WHERE type_agregat = 'GLOBAL'\nFormat optimisé Recharts → [{date, nb_erreurs}]","operationId":"get_evolution_global_evolution_global_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"date_debut","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"YYYY-MM-DD","title":"Date Debut"},"description":"YYYY-MM-DD"},{"name":"date_fin","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"YYYY-MM-DD","title":"Date Fin"},"description":"YYYY-MM-DD"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/evolution/par-monitoring":{"get":{"tags":["Évolution globale"],"summary":"Get Evolution Par Monitoring","description":"Évolution par monitoring — toutes les courbes individuelles.\nSource : VUE_TABLE_FINAL_CONSO WHERE type_agregat = 'MONITO'\n\nRetourne un format pivot par monitoring :\n{\n \"monitorings\": [{\"id\": 1, \"nom\": \"...\", \"serie\": [{\"date\",\"nb_erreurs\"}]}]\n}\nUtile pour un graphique multi-lignes Recharts (une ligne par monitoring).","operationId":"get_evolution_par_monitoring_evolution_par_monitoring_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"date_debut","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"YYYY-MM-DD","title":"Date Debut"},"description":"YYYY-MM-DD"},{"name":"date_fin","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"YYYY-MM-DD","title":"Date Fin"},"description":"YYYY-MM-DD"},{"name":"id_monito","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Filtrer sur un seul monitoring","title":"Id Monito"},"description":"Filtrer sur un seul monitoring"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/evolution/par-service":{"get":{"tags":["Évolution globale"],"summary":"Get Evolution Par Service","description":"Évolution par service (Contrat / Fournisseur) — 1 courbe par service.\nSource : VUE_TABLE_FINAL_CONSO WHERE type_agregat = 'SERVICE'\n\nRetourne :\n{\n \"services\": [{\"service\": \"Contrat\", \"serie\": [{\"date\",\"nb_erreurs\"}]}]\n}","operationId":"get_evolution_par_service_evolution_par_service_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"date_debut","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"YYYY-MM-DD","title":"Date Debut"},"description":"YYYY-MM-DD"},{"name":"date_fin","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"YYYY-MM-DD","title":"Date Fin"},"description":"YYYY-MM-DD"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users":{"get":{"tags":["Admin"],"summary":"List Users","operationId":"list_users_admin_users_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]},"post":{"tags":["Admin"],"summary":"Create User","operationId":"create_user_admin_users_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/admin/users/{id_user}":{"put":{"tags":["Admin"],"summary":"Update User","operationId":"update_user_admin_users__id_user__put","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_user","in":"path","required":true,"schema":{"type":"integer","title":"Id User"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Admin"],"summary":"Delete User","description":"Suppression douce (actif = 0).","operationId":"delete_user_admin_users__id_user__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_user","in":"path","required":true,"schema":{"type":"integer","title":"Id User"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{id_user}/reset-password":{"post":{"tags":["Admin"],"summary":"Reset Password","operationId":"reset_password_admin_users__id_user__reset_password_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_user","in":"path","required":true,"schema":{"type":"integer","title":"Id User"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordReset"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/journal":{"get":{"tags":["Admin"],"summary":"Get Journal","operationId":"get_journal_admin_journal_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":200,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/me/data-export":{"get":{"tags":["RGPD"],"summary":"Export My Data","description":"Droit à la portabilité (art. 20) : export des données de l'utilisateur.","operationId":"export_my_data_me_data_export_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/me":{"delete":{"tags":["RGPD"],"summary":"Delete My Account","description":"Droit à l'oubli (art. 17) : désactivation + anonymisation.","operationId":"delete_my_account_me_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}}},"components":{"schemas":{"Body_login_auth_login_post":{"properties":{"grant_type":{"anyOf":[{"type":"string","pattern":"^password$"},{"type":"null"}],"title":"Grant Type"},"username":{"type":"string","title":"Username"},"password":{"type":"string","format":"password","title":"Password"},"scope":{"type":"string","title":"Scope","default":""},"client_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"},"client_secret":{"anyOf":[{"type":"string"},{"type":"null"}],"format":"password","title":"Client Secret"}},"type":"object","required":["username","password"],"title":"Body_login_auth_login_post"},"CategorieBody":{"properties":{"intitule_categorie":{"type":"string","maxLength":100,"minLength":1,"title":"Intitule Categorie"}},"type":"object","required":["intitule_categorie"],"title":"CategorieBody"},"ContactBody":{"properties":{"id_service":{"type":"integer","title":"Id Service"},"intitule_contact":{"type":"string","maxLength":100,"minLength":1,"title":"Intitule Contact"},"nom":{"type":"string","maxLength":100,"minLength":1,"title":"Nom"},"prenom":{"type":"string","maxLength":100,"minLength":1,"title":"Prenom"},"mail":{"type":"string","maxLength":200,"minLength":1,"title":"Mail"}},"type":"object","required":["id_service","intitule_contact","nom","prenom","mail"],"title":"ContactBody"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"MonitoringBody":{"properties":{"monito_intitule":{"type":"string","maxLength":200,"minLength":1,"title":"Monito Intitule"},"id_service":{"type":"integer","title":"Id Service"},"id_categorie":{"type":"integer","title":"Id Categorie"},"table_source":{"type":"string","maxLength":100,"minLength":1,"title":"Table Source"},"bdd_source":{"type":"string","maxLength":50,"minLength":1,"title":"Bdd Source"}},"type":"object","required":["monito_intitule","id_service","id_categorie","table_source","bdd_source"],"title":"MonitoringBody"},"MonitoringUpdate":{"properties":{"monito_intitule":{"anyOf":[{"type":"string","maxLength":200,"minLength":1},{"type":"null"}],"title":"Monito Intitule"},"id_service":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Id Service"},"id_categorie":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Id Categorie"},"table_source":{"anyOf":[{"type":"string","maxLength":100,"minLength":1},{"type":"null"}],"title":"Table Source"},"bdd_source":{"anyOf":[{"type":"string","maxLength":50,"minLength":1},{"type":"null"}],"title":"Bdd Source"},"actif":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Actif"}},"type":"object","title":"MonitoringUpdate"},"PasswordReset":{"properties":{"password":{"type":"string","title":"Password"}},"type":"object","required":["password"],"title":"PasswordReset"},"ServiceBody":{"properties":{"nom_service":{"type":"string","maxLength":100,"minLength":1,"title":"Nom Service"}},"type":"object","required":["nom_service"],"title":"ServiceBody"},"UserCreate":{"properties":{"username":{"type":"string","title":"Username"},"email":{"type":"string","title":"Email"},"password":{"type":"string","title":"Password"},"role":{"$ref":"#/components/schemas/UserRole"}},"type":"object","required":["username","email","password","role"],"title":"UserCreate"},"UserRole":{"type":"string","enum":["Admin","Superviseur","Consultant"],"title":"UserRole","description":"Rôles applicatifs — miroir de la contrainte CK_USER_ROLE en base."},"UserUpdate":{"properties":{"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"role":{"anyOf":[{"$ref":"#/components/schemas/UserRole"},{"type":"null"}]},"actif":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Actif"}},"type":"object","title":"UserUpdate"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}},"securitySchemes":{"OAuth2PasswordBearer":{"type":"oauth2","flows":{"password":{"scopes":{},"tokenUrl":"auth/login"}}}}}} \ No newline at end of file diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index 96c2958..091b79c 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -66,6 +66,70 @@ def test_monitoring_unknown_returns_404(client, cur, auth_headers): assert r.status_code == 404 +def test_create_user_role_invalide_rejete(client, cur, auth_headers): + # Le rôle est contraint par l'enum UserRole : Pydantic rejette en 422 + # avant d'atteindre la base, sans validation manuelle dans la route. + r = client.post( + "/admin/users", + headers=auth_headers, + json={"username": "x", "email": "x@x.fr", "password": "p", "role": "Root"}, + ) + assert r.status_code == 422 + + +def test_create_user_role_valide_accepte(client, cur, auth_headers): + r = client.post( + "/admin/users", + headers=auth_headers, + json={"username": "x", "email": "x@x.fr", "password": "p", "role": "Superviseur"}, + ) + assert r.status_code == 201 + + +def test_creation_service_reservee_admin(client, cur): + # Un Consultant ne doit pas pouvoir écrire dans le référentiel. + token = create_access_token({"sub": "bob", "role": "Consultant", "uid": 2}) + r = client.post( + "/services", + headers={"Authorization": f"Bearer {token}"}, + json={"nom_service": "Nouveau"}, + ) + assert r.status_code == 403 + + +def test_creation_service_par_admin(client, cur, auth_headers): + cur._one = (42,) + r = client.post("/services", headers=auth_headers, json={"nom_service": "Comptabilité"}) + assert r.status_code == 201 + assert r.json() == {"id_service": 42, "nom_service": "Comptabilité"} + + +def test_suppression_service_refusee_si_rattachements(client, cur, auth_headers): + # 2 monitorings et 1 contact rattachés : la suppression doit être bloquée + # explicitement plutôt que de laisser remonter une violation de clé étrangère. + cur._one = (2, 1) + r = client.delete("/services/1", headers=auth_headers) + assert r.status_code == 409 + assert "2 monitoring(s)" in r.json()["detail"] + + +def test_suppression_service_sans_rattachement(client, cur, auth_headers): + cur._one = (0, 0) + r = client.delete("/services/1", headers=auth_headers) + assert r.status_code == 200 + assert r.json()["status"] == "deleted" + + +def test_desactivation_monitoring_ne_supprime_pas(client, cur, auth_headers): + # TABLE_FINAL référence id_monito : on désactive, on ne supprime jamais. + r = client.delete("/monitorings/1", headers=auth_headers) + assert r.status_code == 200 + assert r.json()["status"] == "deactivated" + requetes = " ".join(q for q, _ in cur.executed) + assert "UPDATE NOMENCLATURE_MONITO SET actif = 0" in requetes + assert "DELETE FROM NOMENCLATURE_MONITO" not in requetes + + def test_admin_forbidden_for_consultant(client): token = create_access_token({"sub": "bob", "role": "Consultant", "uid": 2}) r = client.get("/admin/users", headers={"Authorization": f"Bearer {token}"})