{ "openapi": "3.1.0", "info": { "title": "EnerVision API", "summary": "Collecte, analyse et restitution de séries temporelles énergétiques.", "description": "\nToutes les routes sont préfixées par `/api/v1`.\n\n**Authentification.** Le jeton d'accès se présente dans l'en-tête `Authorization: Bearer ...`.\nLe jeton de rafraîchissement est un cookie `HttpOnly` que le code client ne voit jamais : il\nsuffit d'émettre les requêtes avec les identifiants de session. `POST /auth/refresh` rend un\nnouveau jeton d'accès et fait tourner le cookie.\n\n**Rôles.** `lecteur`, puis `operateur`, puis `admin`. Chaque rôle couvre les droits du\nprécédent.\n\n**Erreurs.** Le corps porte toujours une clé `detail`. Un `403` dont le `detail` vaut\n`password_change_required` n'est pas un refus de droits : il exige le changement du mot de passe\nprovisoire avant toute autre action.\n\nLe parcours de session complet est décrit dans\n`docs/architecture/31-contrat-authentification.md`.\n", "version": "0.1.0", "x-logo": { "url": "/static/logo-icon.png", "altText": "EnerVision" } }, "paths": { "/api/v1/health/live": { "get": { "tags": [ "health" ], "summary": "Sonde de vivacité", "operationId": "liveness_api_v1_health_live_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LivenessStatus" } } } }, "500": { "description": "Erreur interne. `correlation` identifie la trace côté serveur, qui n'est pas renvoyée au client.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InternalErrorResponse" } } } } } } }, "/api/v1/health/ready": { "get": { "tags": [ "health" ], "summary": "Sonde de disponibilité", "operationId": "readiness_api_v1_health_ready_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReadinessStatus" } } } }, "500": { "description": "Erreur interne. `correlation` identifie la trace côté serveur, qui n'est pas renvoyée au client.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InternalErrorResponse" } } } }, "503": { "description": "Base injoignable, ou extension TimescaleDB absente de la base.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/v1/auth/login": { "post": { "tags": [ "auth" ], "summary": "Ouvre une session", "operationId": "login_api_v1_auth_login_post", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LoginRequest" } } }, "required": true }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TokenResponse" } } } }, "500": { "description": "Erreur interne. `correlation` identifie la trace côté serveur, qui n'est pas renvoyée au client.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InternalErrorResponse" } } } }, "422": { "description": "Corps invalide. Le détail nomme le champ fautif et le type d'erreur, jamais la valeur envoyée.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationErrorResponse" } } } }, "401": { "description": "Identifiants faux, compte inconnu ou compte désactivé. Le message est le même dans les trois cas, et n'apprend donc rien sur l'existence du compte.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "429": { "description": "Trop de tentatives sur cette fenêtre glissante.", "headers": { "Retry-After": { "description": "Secondes à attendre avant une nouvelle tentative.", "schema": { "type": "integer" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/v1/auth/refresh": { "post": { "tags": [ "auth" ], "summary": "Fait tourner la session", "operationId": "refresh_api_v1_auth_refresh_post", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TokenResponse" } } } }, "500": { "description": "Erreur interne. `correlation` identifie la trace côté serveur, qui n'est pas renvoyée au client.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InternalErrorResponse" } } } }, "403": { "description": "Origine non autorisée (protection CSRF de `require_trusted_origin`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "401": { "description": "Cookie absent, session expirée, révoquée, ou jeton déjà tourné. Dans ce dernier cas toute la famille de sessions est révoquée et le cookie est effacé avec la réponse.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } }, "security": [ { "Cookie de rafraîchissement": [] } ] } }, "/api/v1/auth/logout": { "post": { "tags": [ "auth" ], "summary": "Ferme la session courante", "operationId": "logout_api_v1_auth_logout_post", "responses": { "204": { "description": "Successful Response" }, "500": { "description": "Erreur interne. `correlation` identifie la trace côté serveur, qui n'est pas renvoyée au client.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InternalErrorResponse" } } } }, "403": { "description": "Origine non autorisée (protection CSRF de `require_trusted_origin`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } }, "security": [ { "Cookie de rafraîchissement": [] } ] } }, "/api/v1/auth/logout-all": { "post": { "tags": [ "auth" ], "summary": "Ferme toutes les sessions du compte", "operationId": "logout_all_api_v1_auth_logout_all_post", "responses": { "204": { "description": "Successful Response" }, "500": { "description": "Erreur interne. `correlation` identifie la trace côté serveur, qui n'est pas renvoyée au client.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InternalErrorResponse" } } } }, "401": { "description": "Jeton absent, illisible, périmé, ou rendu caduc par un changement de rôle ou une désactivation. L'en-tête `WWW-Authenticate` porte la cause dans `error=`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "403": { "description": "Origine non autorisée (protection CSRF de `require_trusted_origin`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } }, "security": [ { "Jeton d'accès": [] } ] } }, "/api/v1/auth/me": { "get": { "tags": [ "auth" ], "summary": "Décrit le compte connecté", "operationId": "me_api_v1_auth_me_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PrincipalResponse" } } } }, "500": { "description": "Erreur interne. `correlation` identifie la trace côté serveur, qui n'est pas renvoyée au client.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InternalErrorResponse" } } } }, "401": { "description": "Jeton absent, illisible, périmé, ou rendu caduc par un changement de rôle ou une désactivation. L'en-tête `WWW-Authenticate` porte la cause dans `error=`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } }, "security": [ { "Jeton d'accès": [] } ] } }, "/api/v1/auth/password": { "post": { "tags": [ "auth" ], "summary": "Change son propre mot de passe", "operationId": "change_password_api_v1_auth_password_post", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PasswordChangeRequest" } } }, "required": true }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TokenResponse" } } } }, "500": { "description": "Erreur interne. `correlation` identifie la trace côté serveur, qui n'est pas renvoyée au client.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InternalErrorResponse" } } } }, "422": { "description": "Corps invalide. Le détail nomme le champ fautif et le type d'erreur, jamais la valeur envoyée.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationErrorResponse" } } } }, "403": { "description": "Origine non autorisée (protection CSRF de `require_trusted_origin`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "401": { "description": "Jeton d'accès invalide, ou mot de passe courant faux.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } }, "security": [ { "Jeton d'accès": [] } ] } }, "/api/v1/users": { "get": { "tags": [ "users" ], "summary": "Liste les comptes", "operationId": "list_users_api_v1_users_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "items": { "$ref": "#/components/schemas/UserResponse" }, "type": "array", "title": "Response List Users Api V1 Users Get" } } } }, "500": { "description": "Erreur interne. `correlation` identifie la trace côté serveur, qui n'est pas renvoyée au client.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InternalErrorResponse" } } } }, "401": { "description": "Jeton absent, illisible, périmé, ou rendu caduc par un changement de rôle ou une désactivation. L'en-tête `WWW-Authenticate` porte la cause dans `error=`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "403": { "description": "Droits insuffisants, ou mot de passe provisoire à changer quand `detail` vaut `password_change_required`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } }, "security": [ { "Jeton d'accès": [] } ] }, "post": { "tags": [ "users" ], "summary": "Crée un compte avec un mot de passe provisoire", "operationId": "create_user_api_v1_users_post", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserCreateRequest" } } }, "required": true }, "responses": { "201": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TemporaryPasswordResponse" } } } }, "500": { "description": "Erreur interne. `correlation` identifie la trace côté serveur, qui n'est pas renvoyée au client.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InternalErrorResponse" } } } }, "401": { "description": "Jeton absent, illisible, périmé, ou rendu caduc par un changement de rôle ou une désactivation. L'en-tête `WWW-Authenticate` porte la cause dans `error=`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "403": { "description": "Droits insuffisants, ou mot de passe provisoire à changer quand `detail` vaut `password_change_required`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "422": { "description": "Corps invalide. Le détail nomme le champ fautif et le type d'erreur, jamais la valeur envoyée.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationErrorResponse" } } } }, "409": { "description": "Adresse déjà portée par un autre compte.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } }, "security": [ { "Jeton d'accès": [] } ] } }, "/api/v1/users/{user_id}": { "patch": { "tags": [ "users" ], "summary": "Change le rôle ou l'activation", "operationId": "update_user_api_v1_users__user_id__patch", "security": [ { "Jeton d'accès": [] } ], "parameters": [ { "name": "user_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid", "title": "User Id" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserUpdateRequest" } } } }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserResponse" } } } }, "500": { "description": "Erreur interne. `correlation` identifie la trace côté serveur, qui n'est pas renvoyée au client.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InternalErrorResponse" } } } }, "401": { "description": "Jeton absent, illisible, périmé, ou rendu caduc par un changement de rôle ou une désactivation. L'en-tête `WWW-Authenticate` porte la cause dans `error=`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "403": { "description": "Droits insuffisants, ou mot de passe provisoire à changer quand `detail` vaut `password_change_required`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "422": { "description": "Corps invalide. Le détail nomme le champ fautif et le type d'erreur, jamais la valeur envoyée.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationErrorResponse" } } } }, "404": { "description": "Aucun compte ne porte cet identifiant.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "400": { "description": "Corps vide, aucune modification demandée.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "409": { "description": "L'opération laisserait la plateforme sans administrateur actif, qu'il s'agisse de rétrograder le dernier ou de le désactiver.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/v1/users/{user_id}/password-reset": { "post": { "tags": [ "users" ], "summary": "Réinitialise le mot de passe et ferme les sessions", "operationId": "reset_password_api_v1_users__user_id__password_reset_post", "security": [ { "Jeton d'accès": [] } ], "parameters": [ { "name": "user_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid", "title": "User Id" } } ], "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TemporaryPasswordResponse" } } } }, "500": { "description": "Erreur interne. `correlation` identifie la trace côté serveur, qui n'est pas renvoyée au client.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InternalErrorResponse" } } } }, "401": { "description": "Jeton absent, illisible, périmé, ou rendu caduc par un changement de rôle ou une désactivation. L'en-tête `WWW-Authenticate` porte la cause dans `error=`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "403": { "description": "Droits insuffisants, ou mot de passe provisoire à changer quand `detail` vaut `password_change_required`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "422": { "description": "Corps invalide. Le détail nomme le champ fautif et le type d'erreur, jamais la valeur envoyée.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationErrorResponse" } } } }, "404": { "description": "Aucun compte ne porte cet identifiant.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/v1/sites": { "get": { "tags": [ "sites" ], "summary": "Liste les sites", "operationId": "list_sites_api_v1_sites_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "items": { "$ref": "#/components/schemas/SiteResponse" }, "type": "array", "title": "Response List Sites Api V1 Sites Get" } } } }, "500": { "description": "Erreur interne. `correlation` identifie la trace côté serveur, qui n'est pas renvoyée au client.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InternalErrorResponse" } } } }, "401": { "description": "Jeton absent, illisible, périmé, ou rendu caduc par un changement de rôle ou une désactivation. L'en-tête `WWW-Authenticate` porte la cause dans `error=`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "403": { "description": "Mot de passe provisoire à changer (`detail` vaut `password_change_required`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } }, "security": [ { "Jeton d'accès": [] } ] } }, "/api/v1/sites/{site_id}": { "get": { "tags": [ "sites" ], "summary": "Décrit un site", "operationId": "get_site_api_v1_sites__site_id__get", "security": [ { "Jeton d'accès": [] } ], "parameters": [ { "name": "site_id", "in": "path", "required": true, "schema": { "type": "string", "title": "Site Id" } } ], "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SiteResponse" } } } }, "500": { "description": "Erreur interne. `correlation` identifie la trace côté serveur, qui n'est pas renvoyée au client.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InternalErrorResponse" } } } }, "401": { "description": "Jeton absent, illisible, périmé, ou rendu caduc par un changement de rôle ou une désactivation. L'en-tête `WWW-Authenticate` porte la cause dans `error=`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "403": { "description": "Mot de passe provisoire à changer (`detail` vaut `password_change_required`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "422": { "description": "Corps invalide. Le détail nomme le champ fautif et le type d'erreur, jamais la valeur envoyée.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationErrorResponse" } } } }, "404": { "description": "Aucun site ne porte cet identifiant.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/v1/alerts": { "get": { "tags": [ "alerts" ], "summary": "Liste les alertes", "operationId": "list_alerts_api_v1_alerts_get", "security": [ { "Jeton d'accès": [] } ], "parameters": [ { "name": "site_id", "in": "query", "required": false, "schema": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Site Id" } }, { "name": "severity", "in": "query", "required": false, "schema": { "anyOf": [ { "$ref": "#/components/schemas/AlertSeverity" }, { "type": "null" } ], "title": "Severity" } } ], "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/AlertResponse" }, "title": "Response List Alerts Api V1 Alerts Get" } } } }, "500": { "description": "Erreur interne. `correlation` identifie la trace côté serveur, qui n'est pas renvoyée au client.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InternalErrorResponse" } } } }, "401": { "description": "Jeton absent, illisible, périmé, ou rendu caduc par un changement de rôle ou une désactivation. L'en-tête `WWW-Authenticate` porte la cause dans `error=`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "403": { "description": "Mot de passe provisoire à changer (`detail` vaut `password_change_required`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "422": { "description": "Corps invalide. Le détail nomme le champ fautif et le type d'erreur, jamais la valeur envoyée.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationErrorResponse" } } } } } } }, "/api/v1/recommendations": { "get": { "tags": [ "recommendations" ], "summary": "Liste les recommandations", "operationId": "list_recommendations_api_v1_recommendations_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "items": { "$ref": "#/components/schemas/RecommendationResponse" }, "type": "array", "title": "Response List Recommendations Api V1 Recommendations Get" } } } }, "500": { "description": "Erreur interne. `correlation` identifie la trace côté serveur, qui n'est pas renvoyée au client.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InternalErrorResponse" } } } }, "401": { "description": "Jeton absent, illisible, périmé, ou rendu caduc par un changement de rôle ou une désactivation. L'en-tête `WWW-Authenticate` porte la cause dans `error=`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "403": { "description": "Mot de passe provisoire à changer (`detail` vaut `password_change_required`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } }, "security": [ { "Jeton d'accès": [] } ] } }, "/api/v1/recommendations/{recommendation_id}": { "get": { "tags": [ "recommendations" ], "summary": "Décrit une recommandation", "operationId": "get_recommendation_api_v1_recommendations__recommendation_id__get", "security": [ { "Jeton d'accès": [] } ], "parameters": [ { "name": "recommendation_id", "in": "path", "required": true, "schema": { "type": "integer", "title": "Recommendation Id" } } ], "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RecommendationResponse" } } } }, "500": { "description": "Erreur interne. `correlation` identifie la trace côté serveur, qui n'est pas renvoyée au client.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InternalErrorResponse" } } } }, "401": { "description": "Jeton absent, illisible, périmé, ou rendu caduc par un changement de rôle ou une désactivation. L'en-tête `WWW-Authenticate` porte la cause dans `error=`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "403": { "description": "Mot de passe provisoire à changer (`detail` vaut `password_change_required`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "422": { "description": "Corps invalide. Le détail nomme le champ fautif et le type d'erreur, jamais la valeur envoyée.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationErrorResponse" } } } }, "404": { "description": "Aucune recommandation ne porte cet identifiant.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/v1/stats/summary": { "get": { "tags": [ "stats" ], "summary": "Résume la consommation instantanée du parc", "operationId": "get_summary_api_v1_stats_summary_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StatsSummaryResponse" } } } }, "500": { "description": "Erreur interne. `correlation` identifie la trace côté serveur, qui n'est pas renvoyée au client.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InternalErrorResponse" } } } }, "401": { "description": "Jeton absent, illisible, périmé, ou rendu caduc par un changement de rôle ou une désactivation. L'en-tête `WWW-Authenticate` porte la cause dans `error=`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "403": { "description": "Mot de passe provisoire à changer (`detail` vaut `password_change_required`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } }, "security": [ { "Jeton d'accès": [] } ] } }, "/api/v1/sensors/status": { "get": { "tags": [ "sensors" ], "summary": "État de santé des capteurs par site", "operationId": "get_status_api_v1_sensors_status_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SensorStatusResponse" } } } }, "500": { "description": "Erreur interne. `correlation` identifie la trace côté serveur, qui n'est pas renvoyée au client.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InternalErrorResponse" } } } }, "401": { "description": "Jeton absent, illisible, périmé, ou rendu caduc par un changement de rôle ou une désactivation. L'en-tête `WWW-Authenticate` porte la cause dans `error=`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "403": { "description": "Droits insuffisants, ou mot de passe provisoire à changer quand `detail` vaut `password_change_required`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } }, "security": [ { "Jeton d'accès": [] } ] } } }, "components": { "schemas": { "AccountKind": { "type": "string", "enum": [ "human", "service" ], "title": "AccountKind" }, "AlertResponse": { "properties": { "alert_id": { "type": "integer", "title": "Alert Id" }, "site_id": { "type": "string", "title": "Site Id" }, "timestamp": { "type": "string", "format": "date-time", "title": "Timestamp" }, "type": { "$ref": "#/components/schemas/AlertType" }, "severity": { "$ref": "#/components/schemas/AlertSeverity" }, "message": { "type": "string", "title": "Message" }, "value": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "title": "Value" }, "threshold": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "title": "Threshold" }, "metric": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Metric" }, "prediction_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "title": "Prediction Id" } }, "type": "object", "required": [ "alert_id", "site_id", "timestamp", "type", "severity", "message", "value", "threshold", "metric", "prediction_id" ], "title": "AlertResponse" }, "AlertSeverity": { "type": "string", "enum": [ "low", "medium", "high", "critical" ], "title": "AlertSeverity" }, "AlertType": { "type": "string", "enum": [ "spike", "threshold", "anomaly", "outage", "sensor" ], "title": "AlertType" }, "ErrorResponse": { "properties": { "detail": { "type": "string", "title": "Detail" } }, "type": "object", "required": [ "detail" ], "title": "ErrorResponse" }, "FieldError": { "properties": { "champ": { "type": "string", "title": "Champ" }, "type": { "type": "string", "title": "Type" } }, "type": "object", "required": [ "champ", "type" ], "title": "FieldError" }, "InternalErrorResponse": { "properties": { "detail": { "type": "string", "title": "Detail" }, "correlation": { "type": "string", "title": "Correlation" } }, "type": "object", "required": [ "detail", "correlation" ], "title": "InternalErrorResponse" }, "LivenessStatus": { "properties": { "status": { "type": "string", "const": "ok", "title": "Status" }, "service": { "type": "string", "title": "Service" }, "version": { "type": "string", "title": "Version" }, "environment": { "type": "string", "title": "Environment" } }, "type": "object", "required": [ "status", "service", "version", "environment" ], "title": "LivenessStatus" }, "LoginRequest": { "properties": { "email": { "type": "string", "format": "email", "title": "Email" }, "password": { "type": "string", "maxLength": 128, "minLength": 1, "title": "Password" } }, "type": "object", "required": [ "email", "password" ], "title": "LoginRequest" }, "PasswordChangeRequest": { "properties": { "current_password": { "type": "string", "maxLength": 128, "minLength": 1, "title": "Current Password" }, "new_password": { "type": "string", "maxLength": 128, "minLength": 12, "title": "New Password" } }, "type": "object", "required": [ "current_password", "new_password" ], "title": "PasswordChangeRequest" }, "PrincipalResponse": { "properties": { "id": { "type": "string", "format": "uuid", "title": "Id" }, "email": { "type": "string", "title": "Email" }, "role": { "$ref": "#/components/schemas/Role" }, "kind": { "$ref": "#/components/schemas/AccountKind" }, "must_change_password": { "type": "boolean", "title": "Must Change Password" } }, "type": "object", "required": [ "id", "email", "role", "kind", "must_change_password" ], "title": "PrincipalResponse" }, "ReadinessStatus": { "properties": { "status": { "type": "string", "const": "ready", "title": "Status" }, "database": { "type": "string", "const": "reachable", "title": "Database" }, "timescaledb": { "type": "string", "const": "loaded", "title": "Timescaledb" } }, "type": "object", "required": [ "status", "database", "timescaledb" ], "title": "ReadinessStatus" }, "RecommendationResponse": { "properties": { "recommendation_id": { "type": "integer", "title": "Recommendation Id" }, "alert_id": { "type": "integer", "title": "Alert Id" }, "action": { "type": "string", "title": "Action" }, "explanation": { "type": "string", "title": "Explanation" }, "rule_reference": { "type": "string", "title": "Rule Reference" }, "created_at": { "type": "string", "format": "date-time", "title": "Created At" } }, "type": "object", "required": [ "recommendation_id", "alert_id", "action", "explanation", "rule_reference", "created_at" ], "title": "RecommendationResponse" }, "Role": { "type": "string", "enum": [ "lecteur", "operateur", "admin" ], "title": "Role" }, "SensorDiagnosticResponse": { "properties": { "status": { "type": "string", "enum": [ "ok", "failing" ], "title": "Status" }, "since": { "anyOf": [ { "type": "string", "format": "date-time" }, { "type": "null" } ], "title": "Since", "description": "Horodatage de la dernière lecture reçue pour ce site. Ce n'est pas le début de la panne : l'historique ne permet pas de le dater sans requête supplémentaire." } }, "type": "object", "required": [ "status", "since" ], "title": "SensorDiagnosticResponse" }, "SensorStatusResponse": { "properties": { "timestamp": { "type": "string", "format": "date-time", "title": "Timestamp" }, "sites": { "items": { "$ref": "#/components/schemas/SiteSensorStatusResponse" }, "type": "array", "title": "Sites" } }, "type": "object", "required": [ "timestamp", "sites" ], "title": "SensorStatusResponse" }, "SiteResponse": { "properties": { "site_id": { "type": "string", "title": "Site Id" }, "site_name": { "type": "string", "title": "Site Name" }, "site_type": { "type": "string", "title": "Site Type" }, "location": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Location" }, "capacity_kw": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "title": "Capacity Kw" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Status" } }, "type": "object", "required": [ "site_id", "site_name", "site_type", "location", "capacity_kw", "status" ], "title": "SiteResponse" }, "SiteSensorStatusResponse": { "properties": { "site_id": { "type": "string", "title": "Site Id" }, "site_name": { "type": "string", "title": "Site Name" }, "sensors": { "$ref": "#/components/schemas/SiteSensorsResponse" }, "overall": { "type": "string", "enum": [ "ok", "degraded", "critical" ], "title": "Overall" } }, "type": "object", "required": [ "site_id", "site_name", "sensors", "overall" ], "title": "SiteSensorStatusResponse" }, "SiteSensorsResponse": { "properties": { "consumption": { "$ref": "#/components/schemas/SensorDiagnosticResponse" }, "electrical": { "$ref": "#/components/schemas/SensorDiagnosticResponse" }, "temperature": { "$ref": "#/components/schemas/SensorDiagnosticResponse" }, "humidity": { "$ref": "#/components/schemas/SensorDiagnosticResponse" }, "network": { "$ref": "#/components/schemas/SensorDiagnosticResponse" } }, "type": "object", "required": [ "consumption", "electrical", "temperature", "humidity", "network" ], "title": "SiteSensorsResponse" }, "SiteSummaryResponse": { "properties": { "site_id": { "type": "string", "title": "Site Id" }, "site_name": { "type": "string", "title": "Site Name" }, "current_consumption_kw": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "title": "Current Consumption Kw" }, "capacity_kw": { "type": "number", "title": "Capacity Kw" }, "load_percent": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "title": "Load Percent" }, "data_quality": { "type": "string", "enum": [ "good", "partial", "degraded", "critical" ], "title": "Data Quality" } }, "type": "object", "required": [ "site_id", "site_name", "current_consumption_kw", "capacity_kw", "load_percent", "data_quality" ], "title": "SiteSummaryResponse" }, "StatsSummaryResponse": { "properties": { "timestamp": { "type": "string", "format": "date-time", "title": "Timestamp" }, "total_sites": { "type": "integer", "title": "Total Sites" }, "total_consumption_kw": { "type": "number", "title": "Total Consumption Kw" }, "total_capacity_kw": { "type": "number", "title": "Total Capacity Kw" }, "average_load_percent": { "type": "number", "title": "Average Load Percent" }, "sites": { "items": { "$ref": "#/components/schemas/SiteSummaryResponse" }, "type": "array", "title": "Sites" } }, "type": "object", "required": [ "timestamp", "total_sites", "total_consumption_kw", "total_capacity_kw", "average_load_percent", "sites" ], "title": "StatsSummaryResponse" }, "TemporaryPasswordResponse": { "properties": { "user": { "$ref": "#/components/schemas/UserResponse" }, "temporary_password": { "type": "string", "title": "Temporary Password" } }, "type": "object", "required": [ "user", "temporary_password" ], "title": "TemporaryPasswordResponse" }, "TokenResponse": { "properties": { "access_token": { "type": "string", "title": "Access Token" }, "token_type": { "type": "string", "const": "bearer", "title": "Token Type", "default": "bearer" }, "expires_in": { "type": "integer", "title": "Expires In" }, "principal": { "$ref": "#/components/schemas/PrincipalResponse" } }, "type": "object", "required": [ "access_token", "expires_in", "principal" ], "title": "TokenResponse" }, "UserCreateRequest": { "properties": { "email": { "type": "string", "format": "email", "title": "Email" }, "role": { "$ref": "#/components/schemas/Role" }, "full_name": { "anyOf": [ { "type": "string", "maxLength": 200 }, { "type": "null" } ], "title": "Full Name" } }, "type": "object", "required": [ "email", "role" ], "title": "UserCreateRequest" }, "UserResponse": { "properties": { "id": { "type": "string", "format": "uuid", "title": "Id" }, "email": { "type": "string", "title": "Email" }, "role": { "$ref": "#/components/schemas/Role" }, "kind": { "$ref": "#/components/schemas/AccountKind" }, "is_active": { "type": "boolean", "title": "Is Active" }, "must_change_password": { "type": "boolean", "title": "Must Change Password" }, "full_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Full Name" }, "last_login_at": { "anyOf": [ { "type": "string", "format": "date-time" }, { "type": "null" } ], "title": "Last Login At" }, "created_at": { "type": "string", "format": "date-time", "title": "Created At" } }, "type": "object", "required": [ "id", "email", "role", "kind", "is_active", "must_change_password", "full_name", "last_login_at", "created_at" ], "title": "UserResponse" }, "UserUpdateRequest": { "properties": { "role": { "anyOf": [ { "$ref": "#/components/schemas/Role" }, { "type": "null" } ] }, "is_active": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "title": "Is Active" } }, "type": "object", "title": "UserUpdateRequest" }, "ValidationErrorResponse": { "properties": { "detail": { "items": { "$ref": "#/components/schemas/FieldError" }, "type": "array", "title": "Detail" } }, "type": "object", "required": [ "detail" ], "title": "ValidationErrorResponse" } }, "securitySchemes": { "Cookie de rafraîchissement": { "type": "apiKey", "description": "Cookie `HttpOnly` posé par `/auth/login` et tourné par `/auth/refresh`. Il prend le préfixe `__Secure-` dès que l'API tourne derrière TLS, et n'est émis que vers `/api/v1/auth`.", "in": "cookie", "name": "ev_refresh" }, "Jeton d'accès": { "type": "http", "scheme": "bearer" } } }, "tags": [ { "name": "health", "description": "Sondes d'infrastructure, publiques. `live` prouve que le processus répond, `ready` que la base répond et que l'extension TimescaleDB est chargée." }, { "name": "auth", "description": "Ouverture, rotation et fermeture de session, et changement de son propre mot de passe." }, { "name": "users", "description": "Administration des comptes. Réservé au rôle `admin`." }, { "name": "sites", "description": "Consultation du parc de sites. Accessible à partir du rôle `lecteur`." }, { "name": "alerts", "description": "Consultation des alertes de consommation. Accessible à partir du rôle `lecteur`." }, { "name": "recommendations", "description": "Consultation des recommandations issues des alertes. Accessible à partir du rôle `lecteur`." }, { "name": "stats", "description": "Statistiques agrégées de consommation. Accessible à partir du rôle `lecteur`." }, { "name": "sensors", "description": "État de santé des capteurs par site. Réservé au rôle `admin`." } ] }