Précise l'authentification Bearer (WORKLOG_API_TOKEN), la configuration des secrets et commandes REST Home Assistant, les codes de réponse de l'API et la conservation des événements de présence. Co-authored-by: OpenAI/GPT-5.6-Terra <vibecoder@antoineve.me>
206 lines
12 KiB
Markdown
206 lines
12 KiB
Markdown
# Documentation de l'API REST (Intégration Présence)
|
|
|
|
Ce document fournit la documentation technique, autonome, exacte et vérifiable de l'API REST implémentée dans l'application **Tableau de bord pro**.
|
|
|
|
---
|
|
|
|
## 1. But et périmètre
|
|
|
|
L'API REST expose un endpoint unique destiné à recevoir des événements de présence automatisés (par exemple en provenance d'un système domotique comme Home Assistant). Elle permet de :
|
|
- Enregistrer l'**arrivée** d'un collaborateur sur son lieu de travail (création automatique d'une entrée de journée `WorkEntry` si elle n'existe pas et d'un événement `WorkplacePresenceEvent`).
|
|
- Enregistrer le **départ** du lieu de travail (fermeture de l'arrivée ouverte, création d'une plage horaire `TimeSlot` et de l'événement de départ associé).
|
|
- Garantir l'**idempotence** des requêtes via une clé unique pour éviter les doublons en cas de rejeu réseau.
|
|
|
|
---
|
|
|
|
## 2. Prérequis de configuration (`[home_assistant]`)
|
|
|
|
L'activation de l'API dépend de la présence et de la validité de la section `[home_assistant]` dans le fichier de configuration `config.toml`. Si cette section est absente, l'API est désactivée et renvoie un code `404`.
|
|
|
|
Exemple de configuration valide dans `config.toml` :
|
|
```toml
|
|
[home_assistant]
|
|
timezone = "Europe/Paris"
|
|
default_day_type = "WORK"
|
|
default_journey_profile_id = "moteur_seul"
|
|
default_motor_vehicle_id = "citadine"
|
|
```
|
|
|
|
### Règles de validation de la configuration :
|
|
- **`timezone`** : Doit être un fuseau horaire valide reconnu par `zoneinfo.ZoneInfo` (ex: `"Europe/Paris"`).
|
|
- **`default_day_type`** : Doit correspondre à un type de jour valide (`WORK`, `TT`, `GARDE`, `ASTREINTE`, `FORMATION`, `RTT`, `CONGE`, `MALADE`, `FERIE`).
|
|
- **`default_journey_profile_id`** : Doit être un identifiant de trajet existant dans la section `[journeys]`.
|
|
- **`default_motor_vehicle_id`** : Doit être un identifiant de véhicule existant dans la section `[vehicles]` dont le type est `"moteur"`.
|
|
|
|
---
|
|
|
|
## 3. Sécurité, Authentification et Transport
|
|
|
|
### Authentification par Jeton (Bearer Token)
|
|
- L'API exige un jeton d'authentification transmis via l'en-tête HTTP `Authorization` au format Bearer :
|
|
```http
|
|
Authorization: Bearer <WORKLOG_API_TOKEN>
|
|
```
|
|
- La variable d'environnement **`WORKLOG_API_TOKEN`** doit être définie sur le serveur d'exécution. Si cette variable n'est pas configurée, l'API refuse toute requête et renvoie le statut `503`.
|
|
- La comparaison du jeton fourni avec la valeur configurée est réalisée de manière sécurisée en temps constant via `hmac.compare_digest` pour prévenir les attaques temporelles.
|
|
|
|
### Transport et Proxy (HTTPS / HAProxy)
|
|
- L'application Flask n'implémente pas nativement le chiffrement TLS ni de mécanisme de rotation de jeton.
|
|
- En production, la sécurité du transport (HTTPS) et le routage sont entièrement délégués au serveur amont **HAProxy**.
|
|
|
|
---
|
|
|
|
## 4. Spécifications techniques de l'endpoint
|
|
|
|
- **URL** : `/api/v1/workplace-presence`
|
|
- **Méthode** : `POST`
|
|
- **Content-Type requis** : `application/json`
|
|
- **Taille maximale du corps** : `4096` octets (`_MAX_BODY_BYTES`). Au-delà, l'API rejette la requête avec le code `413`.
|
|
|
|
### Limitation de débit (Rate-Limit)
|
|
- Chaque jeton d'authentification est soumis à une limitation de débit par processus de **30 requêtes par fenêtre glissante de 60 secondes** (`_RATE_LIMIT = 30`, `_RATE_WINDOW_SECONDS = 60.0`).
|
|
- *Note d'architecture* : Ce compteur est strictement local au processus d'exécution Flask/Gunicorn. Dans un déploiement multi-workers, une limitation globale nécessite un mécanisme amont (tel que HAProxy) ou un stockage partagé.
|
|
|
|
---
|
|
|
|
## 5. Structure du Corps de la Requête (JSON)
|
|
|
|
Le corps de la requête doit être un objet JSON valide contenant les champs suivants :
|
|
|
|
| Champ | Type | Obligatoire | Description |
|
|
| :--- | :--- | :--- | :--- |
|
|
| `event` | `string` | Oui | Type d'événement : `"arrival"` ou `"departure"`. |
|
|
| `occurred_at` | `string` | Oui | Horodatage au format ISO 8601 comportant obligatoirement un offset de fuseau horaire (ex: `2026-08-13T08:00:00+02:00`). |
|
|
| `idempotency_key` | `string` | Oui (corps ou en-tête) | Clé d'idempotence unique (limitée à 255 caractères). **Obligatoire** : transmise soit dans le corps JSON (`idempotency_key`), soit via l'en-tête HTTP `X-Idempotency-Key`. |
|
|
|
|
### Règle sur la clé d'idempotence :
|
|
La clé d'idempotence est **obligatoire**. Elle doit être fournie soit dans le corps JSON (`idempotency_key`), soit via l'en-tête HTTP `X-Idempotency-Key`. Si la clé est fournie à la fois dans le corps JSON et dans l'en-tête, **leurs valeurs doivent être strictement identiques**, sous peine d'un rejet avec le code `422`. Ne jamais considérer ce champ comme facultatif.
|
|
|
|
---
|
|
|
|
## 6. Gestion du Temps et Règles Métier
|
|
|
|
- **Fuseau horaire** : Les horodatages `occurred_at` sont convertis dans le fuseau horaire configuré dans `[home_assistant].timezone` (la configuration explicite de `timezone` est obligatoire dans la section `[home_assistant]` ; `"Europe/Paris"` n'est qu'un exemple de valeur fourni dans la configuration de référence du projet).
|
|
- **Arrivée (`arrival`)** :
|
|
- Crée une entrée de journée (`WorkEntry`) à la date locale si elle n'existe pas, en appliquant les valeurs par défaut de la configuration (`default_day_type`, `default_journey_profile_id`, `default_motor_vehicle_id`).
|
|
- Une seule arrivée peut être ouverte (`time_slot_id IS NULL`) simultanément **de manière globale à l'application**.
|
|
- **Départ (`departure`)** :
|
|
- Ferme l'arrivée ouverte correspondante.
|
|
- Crée un intervalle de temps (`TimeSlot`) rattaché à l'entrée de journée, défini entre l'heure de l'arrivée et l'heure du départ.
|
|
- L'heure du départ doit être strictement postérieure à celle de l'arrivée locale (`occurred_local > arrival_local`), sinon un code `422` est renvoyé.
|
|
|
|
---
|
|
|
|
## 7. Réponses de Succès
|
|
|
|
### Codes de statut de succès :
|
|
- **`201 Created`** : Événement d'arrivée créé avec succès (nouvelle ressource).
|
|
- **`200 OK`** : Événement de départ enregistré avec succès, ou **rejeu (replay)** d'un événement existant identique (idempotence).
|
|
|
|
### Format de la réponse JSON :
|
|
```json
|
|
{
|
|
"status": "created",
|
|
"event_id": 42,
|
|
"entry_id": 12,
|
|
"time_slot_id": null,
|
|
"event_type": "arrival"
|
|
}
|
|
```
|
|
*Champs retournés :*
|
|
- `status` : `"created"` (nouvel enregistrement) ou `"replayed"` (requête idempotente rejouée).
|
|
- `event_id` : Identifiant unique de l'événement de présence enregistré.
|
|
- `entry_id` : Identifiant de la journée de travail associée (`WorkEntry`).
|
|
- `time_slot_id` : Identifiant de la plage horaire créée (`null` pour une arrivée non fermée, entier pour un départ).
|
|
- `event_type` : Rappel du type d'événement traité (`"arrival"` ou `"departure"`).
|
|
|
|
---
|
|
|
|
## 8. Gestion des Erreurs
|
|
|
|
L'API renvoie un format d'erreur homogène sous forme d'objet JSON :
|
|
```json
|
|
{
|
|
"error": "Message explicatif de l'erreur"
|
|
}
|
|
```
|
|
Toutes les réponses de l'API (succès et erreurs) comportent l'en-tête `Cache-Control: no-store`.
|
|
|
|
### Codes de statut HTTP d'erreur pris en charge :
|
|
|
|
| Code | Message d'erreur associé | Cause / Description |
|
|
| :--- | :--- | :--- |
|
|
| **`400`** | `"JSON invalide"` | Le corps de la requête n'est pas un JSON syntaxiquement valide. |
|
|
| **`401`** | `"Authentification requise"` | En-tête `Authorization` absent, schéma non Bearer, ou jeton invalide. |
|
|
| **`404`** | `"API indisponible"` | Section `[home_assistant]` absente de `config.toml`. |
|
|
| **`405`** | `"Méthode non autorisée"` | Utilisation d'une méthode HTTP autre que `POST` sur l'endpoint `/api/v1/...`. |
|
|
| **`409`** | `"Conflit métier"` | Conflit logique : tentative d'enregistrement d'un départ sans arrivée ouverte, double arrivée, ou réutilisation d'une clé d'idempotence avec des paramètres différents. |
|
|
| **`413`** | `"Requête trop volumineuse"` | La taille du corps de la requête dépasse la limite de 4096 octets. |
|
|
| **`415`** | `"Content-Type invalide"` | L'en-tête `Content-Type` est absent ou différent de `application/json`. |
|
|
| **`422`** | `"Schéma invalide"` ou `"Événement invalide"` | Données non conformes : clés non autorisées, format `occurred_at` invalide (absence d'offset ou ISO 8601 incorrect), types incorrects, ou départ antérieur à l'arrivée. |
|
|
| **`429`** | `"Trop de requêtes"` | Dépassement de la limite de débit (Rate-limit de 30 req / 60s par processus). |
|
|
| **`500`** | `"Erreur interne"` | Exception inattendue lors du traitement en base de données. |
|
|
| **`503`** | `"API indisponible"` | Variable d'environnement `WORKLOG_API_TOKEN` non définie sur le serveur. |
|
|
|
|
---
|
|
|
|
## 9. Idempotence
|
|
|
|
- L'API garantit l'idempotence par le biais de la **clé d'idempotence obligatoire** (`idempotency_key` dans le corps ou en-tête `X-Idempotency-Key`).
|
|
- Si une requête est rejouée avec une clé d'idempotence déjà enregistrée :
|
|
- Si les paramètres (`event` et `occurred_at`) sont strictement identiques, l'API renvoie le résultat précédent avec le statut `200 OK` et `"status": "replayed"`, sans dupliquer les enregistrements.
|
|
- Si les paramètres diffèrent pour une même clé, l'API rejette la requête avec le code `409` (`"Conflit métier"`).
|
|
|
|
---
|
|
|
|
## 10. Comportement de l'Interface Web après l'API
|
|
|
|
- Les journées (`WorkEntry`) créées automatiquement par l'API suite à une arrivée sont pleinement intégrées dans l'application.
|
|
- L'utilisateur peut consulter et modifier ces entrées via l'interface Web (formulaire d'édition `/entries/<id>/edit`) :
|
|
- Modification du type de journée (ex: passer de `WORK` à `FORMATION`), du profil de trajet ou du véhicule à moteur.
|
|
- Modification, ajout ou suppression de plages horaires (`TimeSlot`).
|
|
- *Cohérence et conservation des événements de présence (comportement validé par les tests d'intégration)* :
|
|
- **Conservation** : Les événements de présence (`WorkplacePresenceEvent`) sont conservés en base de données **tant que l'entrée de journée associée (`WorkEntry`) n'est pas supprimée** (liaison soumise à la suppression en cascade `cascade="all, delete-orphan"` au niveau du modèle de données).
|
|
- **Dissociation de plage** : Lorsque les plages horaires d'une entrée sont modifiées ou recréées depuis l'interface Web (formulaire d'édition), les événements de présence associés (`WorkplacePresenceEvent`) conservent leur traçabilité et leur rattachement à l'entrée `WorkEntry`, mais leurs identifiants de plage (`time_slot_id`) sont dissociés (`NULL`) pour refléter la réorganisation manuelle de la journée, conformément aux tests d'intégration existants. Aucune autre promesse au-delà de cette dissociation (`NULL`), du maintien conditionnel (tant que `WorkEntry` existe) et du suivi des événements n'est garantie.
|
|
|
|
---
|
|
|
|
## 11. Exemples d'utilisation `curl` sécurisés
|
|
|
|
> **Avertissement de sécurité** : N'écrivez jamais de secret en clair dans des scripts ou des documentations. Utilisez toujours une variable shell (ex: `$WORKLOG_API_TOKEN`).
|
|
|
|
### 1. Enregistrer une arrivée :
|
|
```bash
|
|
curl -i -X POST "http://localhost:5000/api/v1/workplace-presence" \
|
|
-H "Authorization: Bearer $WORKLOG_API_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-H "X-Idempotency-Key: arrival-2026-08-13-01" \
|
|
-d '{
|
|
"event": "arrival",
|
|
"occurred_at": "2026-08-13T08:00:00+02:00"
|
|
}'
|
|
```
|
|
|
|
### 2. Enregistrer un départ :
|
|
```bash
|
|
curl -i -X POST "http://localhost:5000/api/v1/workplace-presence" \
|
|
-H "Authorization: Bearer $WORKLOG_API_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-H "X-Idempotency-Key: departure-2026-08-13-01" \
|
|
-d '{
|
|
"event": "departure",
|
|
"occurred_at": "2026-08-13T17:00:00+02:00"
|
|
}'
|
|
```
|
|
|
|
---
|
|
|
|
## 12. Périmètre et Limites Connues
|
|
|
|
- **Fonctionnalités non implémentées** :
|
|
- Pas de support CORS (Cross-Origin Resource Sharing) pour des appels directs depuis un navigateur web.
|
|
- Pas de rotation automatique des jetons d'API.
|
|
- Pas de limite de débit distribuée (le rate-limit est confiné à chaque processus/worker Gunicorn).
|
|
- Pas d'import CSV en masse via l'API REST.
|
|
- **Intégration Home Assistant** : La configuration détaillée des scripts, secrets, commandes REST et automatisations Home Assistant est documentée dans la [Documentation Home Assistant](home-assistant.md).
|