Files
ENI-projet-piscine/apps/backend/openapi.json
T
Johan LEROY 07ea8d21dc feat(backend): expose GET /api/v1/sites/{site_id}/current pour l'issue #29
Ajoute la dernière mesure d'un site (SiteService.current), en réutilisant
la vérification d'existence déjà en place pour GET /sites/{site_id} :
SiteService gagne une dépendance ReadingRepository, sur le modèle de
composition déjà utilisé par StatsService/SensorService. Un site connu
sans lecture rend 200 avec les champs de mesure à null et
data_quality="critical" ; seul un site_id absent rend 404.
2026-09-16 15:25:14 +02:00

2186 lines
61 KiB
JSON

{
"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"
},
"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/sites/{site_id}/current": {
"get": {
"tags": [
"sites"
],
"summary": "Dernière mesure d'un site",
"operationId": "get_current_api_v1_sites__site_id__current_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/SiteCurrentResponse"
}
}
}
},
"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": []
}
]
}
}
},
"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"
},
"SiteCurrentResponse": {
"properties": {
"timestamp": {
"anyOf": [
{
"type": "string",
"format": "date-time"
},
{
"type": "null"
}
],
"title": "Timestamp"
},
"site_id": {
"type": "string",
"title": "Site Id"
},
"site_type": {
"type": "string",
"title": "Site Type"
},
"consumption_kw": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Consumption Kw"
},
"consumption_kwh": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Consumption Kwh"
},
"voltage_v": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Voltage V"
},
"current_a": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Current A"
},
"power_factor": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Power Factor"
},
"temperature_celsius": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Temperature Celsius"
},
"humidity_percent": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Humidity Percent"
},
"null_reasons": {
"items": {
"type": "string"
},
"type": "array",
"title": "Null Reasons"
},
"data_quality": {
"type": "string",
"enum": [
"good",
"partial",
"degraded",
"critical"
],
"title": "Data Quality"
}
},
"type": "object",
"required": [
"timestamp",
"site_id",
"site_type",
"consumption_kw",
"consumption_kwh",
"voltage_v",
"current_a",
"power_factor",
"temperature_celsius",
"humidity_percent",
"null_reasons",
"data_quality"
],
"title": "SiteCurrentResponse"
},
"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"
},
"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`."
}
]
}