# Api-DataSentinel API **FastAPI** de monitoring de la qualité des données — Data Sentinel (XEFI). Backend en lecture seule sur SQL Server, sécurisé par authentification JWT. ## Stack - Python 3.12 · FastAPI 0.136 · uvicorn - SQL Server 2022 via `pyodbc` (ODBC Driver 18) - Auth : JWT (`python-jose`) + bcrypt (`passlib`), rate-limit (`slowapi`) ## Lancement en local (dev) ```powershell python -m venv .venv .\.venv\Scripts\Activate.ps1 pip install -r requirements.txt 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 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 (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 — **obligatoire en déploiement** | clé aléatoire régénérée à chaque démarrage | | `APP_BUILD` | SHA du commit construit, injecté par la CI et exposé par `/health` | `local` | | `JWT_ALGORITHM` / `JWT_EXPIRE_MINUTES` | algo / durée du token | `HS256` / `60` | ## Authentification & rôles - `POST /auth/login` (form `username`/`password`) → `{access_token, token_type, user}`. Rate-limité **5/min**. - `GET /auth/me` → profil courant. - **Tous les endpoints de données exigent** `Authorization: Bearer ` (401 sinon). Seul `/health` est public. - Rôles : `Admin`, `Superviseur`, `Consultant`. Les routes `/admin/*` exigent `Admin` (403 sinon). ### Comptes de démo | Identifiant | Mot de passe | Rôle | Usage | |-------------|--------------|------|-------| | `Juré` (`JURE@NEXA.com`) | `123456` | Admin | **Compte de visite / évaluation** — à communiquer aux personnes qui consultent le site | | `admin` | `Admin2026!` | Admin | Compte d'administration technique | | `superviseur` | `Super2026!` | Superviseur | Illustration du RBAC (pas d'accès `/admin/*`) | | `consultant` | `Conseil2026!` | Consultant | Illustration du RBAC (lecture seule) | Ces comptes sont créés par `sql/data_sentinel_auth.sql`. Pour (re)créer le seul compte `Juré` sur une base déjà déployée, sans rejouer tout le script d'auth : ```bash sqlcmd -S -d DataSentinel -U -P -i sql/create_admin_jure.sql ``` 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). Spécification complète : `GET /openapi.json` (export dans `docs/openapi.json`). ## Vérifier qu'un déploiement a pris `GET /health` expose le SHA du commit dont l'image a été construite : ```bash curl -s https://datasentinel-api.nfteam.ovh/health # {"api":"ok","database":"ok","version":"1.0.0","build":"cb84e20e1f2a",...} ``` Comparer `build` au dernier commit poussé sur `main`. S'ils diffèrent, le conteneur tourne encore une ancienne image : `docker compose pull` puis `docker compose up -d` (un `up -d` seul ne retélécharge rien). Le front expose la même information sur `/version.json`. ## Sécurité - `JWT_SECRET` doit être défini en déploiement. À défaut, l'API démarre quand même mais tire une clé aléatoire à chaque lancement (sessions perdues au redémarrage) : aucun secret de repli n'est écrit dans le dépôt, un secret public permettrait de forger un jeton d'administrateur. - En-têtes : `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Strict-Transport-Security`. - CORS restreint aux origines `CORS_ORIGINS`, tous verbes + credentials. - Requêtes SQL **paramétrées** (noms de tables/colonnes whitelistés) ; le compte applicatif n'est pas `sa`. Tables d'auth : `[USER]` + `JOURNAL_AUDIT` (`sql/data_sentinel_auth.sql`, identique au dump livré dans `RENDU/02_Dump_SQL/`). - Journal d'audit alimenté à chaque login + action admin. ## Tests ```bash pip install pytest httpx && pytest -q ``` **82 tests**, curseur SQL simulé (aucune vraie base requise) : | Fichier | Couverture | |---------|------------| | `test_endpoints.py` | santé, référentiels, authentification, filtres du dashboard, rôles par enum | | `test_historique.py` | bornes de dates, validation du format, agrégation des courbes par monitoring et par service | | `test_securite.py` | protection 401 de chaque route, RBAC 403, jeton forgé rejeté, en-têtes de sécurité, rate-limit 429, compte désactivé, droits RGPD | | `test_referentiels.py` | CRUD complet, refus 409 sur rattachement, modification partielle, paramétrage des requêtes de recherche | Le compteur anti brute-force est remis à zéro entre les tests (fixture `limiteur_vierge`) : c'est un état global, et sans cela les tests de connexion se comptabilisent entre eux et échouent selon l'ordre d'exécution. ## Docker & CI/CD - `Dockerfile` : image Python 3.12 + `msodbcsql18`. - CI Gitea Actions (`.gitea/workflows/build.yml`) : **build → pytest (dans l'image) → push** vers `git.nfteam.ovh/neckfire/datasentinel-api` + notification ntfy. Déclenché sur push `main`. - Déploiement, exploitation, sauvegarde : voir `RUNBOOK.md` et le stack d'hébergement (`homelab/dev/datasentinel/`). La base tourne sur un **SQL Server partagé** (`dev-mssql`), pas dédiée à l'app.