# Documentation API Auth

Salut, voici comment utiliser les endpoints d'authentification que j'ai mis en place.

## URL de base

```
https://api.aircarto.fr/auth
```

## Authentification

Le token JWT est stocké **uniquement** dans un cookie HttpOnly nommé `aircarto_token` (valide 24h).
Pour les appels depuis un navigateur, il faut envoyer les cookies (`credentials: "include"` côté fetch).

```
Cookie: aircarto_token=<ton_token>
```

---

## 1. Login - Connexion

**POST** `https://api.aircarto.fr/auth/login.php`

Tu envoies ça en JSON :

```json
{
  "username": "ton_username",
  "password": "ton_password"
}
```

Petit truc : le champ `username` accepte aussi un email, ça marche dans les deux sens.

Tu reçois en retour :

```json
{
  "message": "Login successful",
  "user": {
    "id": 1,
    "username": "ton_username",
    "email": "user@example.com",
    "full_name": "Nom Complet",
    "role": "client",
    "organization_id": 1
  }
}
```

Si ça merde, tu auras une erreur 400 ou 401 avec un message.

**Exemple curl :**

```bash
curl -X POST https://api.aircarto.fr/auth/login.php \
  -H "Content-Type: application/json" \
  -c cookie.txt \
  -d '{"username": "test", "password": "test123"}'
```

**Exemple JS :**

```javascript
const res = await fetch("https://api.aircarto.fr/auth/login.php", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    username: "test",
    password: "test123",
  }),
});

const data = await res.json();
if (res.ok) {
  localStorage.setItem("token", data.token);
}
```

---

## 2. Register - Inscription

**POST** `https://api.aircarto.fr/auth/register.php`

Tu envoies :

```json
{
  "username": "nouveau_user",
  "email": "user@example.com",
  "password": "password123",
  "full_name": "Nom Complet",
  "invitation_token": "token_client",
  "admin_invitation_token": "token_admin",
  "organization_id": 1
}
```

Les champs obligatoires : `username`, `email`, `password`. Le compte créé sans invitation est toujours un **client**.
Si tu passes `invitation_token`, le compte est **client** et l'organisation vient de l'invitation.
Si tu passes `admin_invitation_token`, le rôle et l'organisation viennent de l'invitation (admin/master).

Réponse si ça marche (201) :

```json
{
  "message": "User registered successfully",
  "user": {
    "id": 1,
    "username": "nouveau_user",
    "email": "user@example.com",
    "full_name": "Nom Complet",
    "role": "client",
    "organization_id": 1
  }
}
```

Si le username ou l'email existe déjà, tu auras une 409.

**Exemple JS :**

```javascript
const res = await fetch("https://api.aircarto.fr/auth/register.php", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  credentials: "include",
  body: JSON.stringify({
    username: "nouveau_user",
    email: "user@example.com",
    password: "password123",
    full_name: "Nom Complet",
  }),
});

const data = await res.json();
```

---

## 3. Verify - Vérifier le token

**GET** `https://api.aircarto.fr/auth/verify.php`

Le token est lu depuis le cookie HttpOnly `aircarto_token` (envoyé automatiquement par le navigateur).

Réponse si le token est valide :

```json
{
  "message": "Token is valid",
  "user": {
    "id": 1,
    "username": "ton_username",
    "email": "user@example.com",
    "full_name": "Nom Complet",
    "role": "client",
    "organization_id": 1,
    "impersonated_by": null
  }
}
```

Si le token est expiré ou invalide, tu auras une 401.

**Exemple JS :**

```javascript
const res = await fetch("https://api.aircarto.fr/auth/verify.php", {
  credentials: "include",
});

---

## 3bis. Logout - Déconnexion

**POST** `https://api.aircarto.fr/auth/logout.php`

Supprime le cookie HttpOnly `aircarto_token`.

```json
{
  "message": "Logged out"
}
```

**Exemple JS :**

```javascript
await fetch("https://api.aircarto.fr/auth/logout.php", {
  method: "POST",
  credentials: "include",
});
```

const data = await res.json();
if (!res.ok) {
  // Token invalide, faut se reconnecter
  localStorage.removeItem("token");
}
```

---

## 4. Invite Admin/Master

**POST** `https://api.aircarto.fr/auth/invite-admin.php`

Crée une invitation pour un compte admin ou master (auth requise, admin ou master).

```json
{
  "email": "new.admin@example.com",
  "suggested_username": "new_admin",
  "role": "admin",
  "organization_id": 1,
  "sensor_ids": [12, 15],
  "expires_in_hours": 168
}
```

- `sensor_ids` (optionnel) : tableau d’ids **ou de noms** de capteurs à pré-attribuer au compte.
- `expires_in_hours` (optionnel) : durée de validité en heures ; défaut **168** (1 semaine).

Notes :

- Un **admin** ne peut inviter que des **admins** de sa propre organisation.
- Un **master** peut inviter des **admins** ou des **masters**.

Réponse :

```json
{
  "message": "Admin invitation created",
  "invitation": {
    "id": 10,
    "email": "new.admin@example.com",
    "suggested_username": "new_admin",
    "role": "admin",
    "organization_id": 1,
    "token": "token_admin",
    "expires_at": "2026-02-05 12:00:00",
    "status": "pending"
  }
}
```

En dev, l’email est écrit dans le fichier `"/tmp/aircarto_mail.log"` (ou la variable `MAIL_LOG_PATH`).

---

## 5. Invite Client

**POST** `https://api.aircarto.fr/auth/invite-client.php`

Crée une invitation pour un client (auth requise, admin ou master).

```json
{
  "email": "client@example.com",
  "suggested_username": "client_x",
  "organization_id": 1,
  "sensor_ids": [12, 15],
  "project_ids": [101, 102],
  "expires_in_hours": 168
}
```

- `sensor_ids` (optionnel) : tableau d’ids **ou de noms** de capteurs à pré-attribuer au compte.
- `project_ids` (optionnel) : tableau d’ids de projets à pré-attribuer au client (même logique que la préattribution capteurs).
- `expires_in_hours` (optionnel) : durée de validité en heures ; défaut **168** (1 semaine).

Réponse :

```json
{
  "message": "Client invitation created",
  "invitation": {
    "id": 20,
    "email": "client@example.com",
    "suggested_username": "client_x",
    "token": "token_client",
    "expires_at": "2026-02-05 12:00:00",
    "status": "pending",
    "organization_id": 1
  }
}
```

En dev, l’email est écrit dans le fichier `"/tmp/aircarto_mail.log"` (ou la variable `MAIL_LOG_PATH`).

---

## 6. Invitation - Préremplissage

**GET** `https://api.aircarto.fr/auth/invitation.php`

Permet de récupérer les infos d'une invitation pour préremplir le formulaire (email, suggested username).

**Client :**

```
https://api.aircarto.fr/auth/invitation.php?invitation_token=token_client
```

**Admin/Master :**

```
https://api.aircarto.fr/auth/invitation.php?admin_invitation_token=token_admin
```

Réponse :

```json
{
  "invitation": {
    "type": "client",
    "email": "client@example.com",
    "suggested_username": "client_x",
    "role": "client",
    "organization_id": 1,
    "expires_at": "2026-02-05 12:00:00",
    "status": "pending"
  }
}
```

Notes :

- L’email d’invitation doit être utilisé tel quel lors du register (non modifiable).
- Le champ `suggested_username` est une suggestion uniquement.

---

## 6.3 Projects - CRUD projets

**GET** `https://api.aircarto.fr/auth/projects.php`

- **master/admin** : liste des projets (admin = uniquement son organisation)
- **client** : uniquement les projets où il est invité

Paramètre optionnel : `organization_id` (master uniquement).

Réponse : `{ "projects": [ ... ] }`. Chaque projet contient notamment :
- `id`, `organization_id`, `name`, `start_date`, `end_date`, `created_by`, `created_by_username`, `created_at`, `updated_at`
- **`aircarto_sensors`** : tableau de capteurs Aircarto (ModuleAir, NebuleAir, MobileAir). Chaque élément contient : `id`, `nom`, **`token`** (à utiliser pour les appels API données : metadata, dataModuleAir, etc.), `alias`, `display_name`, `capteur_type`, `owner`, `displaymap`
- **`sigicom_sensors`** : tableau de capteurs Sigicom. Chaque élément contient : `type`, `serial`, `alias`, `display_name`
- `aircarto_sensor_count`, `sigicom_sensor_count`

**Pour le front (dashboard monitoring)** : les capteurs Aircarto incluent le champ `token` pour permettre d’appeler directement les endpoints de données (ex. `/capteurs/dataModuleAir.php`, metadata) sans appel supplémentaire.

**POST** `https://api.aircarto.fr/auth/projects.php`

Crée un projet (admin/master).

```json
{
  "organization_id": 1,
  "name": "Chantier A",
  "start_date": "2026-02-20",
  "end_date": "2026-05-30",
  "sensor_ids": [12, 15],
  "sigicom_devices": [
    { "type": "NOISE", "serial": "SG-10001" }
  ]
}
```

Notes :

- `sensor_ids` accepte uniquement des capteurs Aircarto (`nebuleair`, `moduleair`, `mobileair`).
- `sigicom_devices` permet d’associer des capteurs Sigicom.
- `sensor_ids` et `sigicom_devices` sont optionnels.

**PUT** `https://api.aircarto.fr/auth/projects.php`

Met à jour un projet (admin/master). `id` requis.

```json
{
  "id": 101,
  "name": "Chantier A - phase 2",
  "start_date": "2026-03-01",
  "end_date": "2026-06-01",
  "sensor_ids": [12, 25],
  "sigicom_devices": []
}
```

Si `sensor_ids` est fourni, la liste Aircarto du projet est remplacée.  
Si `sigicom_devices` est fourni, la liste Sigicom du projet est remplacée.

**DELETE** `https://api.aircarto.fr/auth/projects.php`

```json
{
  "id": 101
}
```
Q

## 6.4 Projects - Gestion des clients invités

**GET** `https://api.aircarto.fr/auth/project-clients.php?project_id=101`

Liste les clients invités sur un projet (admin/master).

**POST** `https://api.aircarto.fr/auth/project-clients.php`

Invite/ajoute un client existant sur un projet (admin/master).

```json
{
  "project_id": 101,
  "user_id": 55
}
```

**DELETE** `https://api.aircarto.fr/auth/project-clients.php`

Retire un client d’un projet.

```json
{
  "project_id": 101,
  "user_id": 55
}
```

---

## 6.5 Alias capteurs par organisation

**GET** `https://api.aircarto.fr/auth/sensor-aliases.php?organization_id=1`

Liste les alias capteurs de l’organisation (admin/master, admin limité à sa propre org).

**PUT** `https://api.aircarto.fr/auth/sensor-aliases.php`

Créer/mettre à jour un alias Aircarto (avec **sensor_id** numérique ou **sensor_name** — nom unique du capteur) :

```json
{
  "organization_id": 1,
  "sensor_id": 12,
  "alias": "Capteur toiture mairie"
}
```

Ou avec le nom du capteur :

```json
{
  "organization_id": 4,
  "sensor_name": "nebuleair-pro010",
  "alias": "Capteur toiture mairie"
}
```

Créer/mettre à jour un alias Sigicom :

```json
{
  "organization_id": 1,
  "sigicom_sensor_type": "NOISE",
  "sigicom_sensor_serial": "SG-10001",
  "alias": "Sonomètre chantier nord"
}
```

**DELETE** `https://api.aircarto.fr/auth/sensor-aliases.php`

Suppression alias Aircarto (**sensor_id** ou **sensor_name**) :

```json
{
  "organization_id": 1,
  "sensor_id": 12
}
```

Ou avec le nom : `"sensor_name": "nebuleair-pro010"`.

Suppression alias Sigicom :

```json
{
  "organization_id": 1,
  "sigicom_sensor_type": "NOISE",
  "sigicom_sensor_serial": "SG-10001"
}
```

### Réponse `GET /sigicom/sensor-data.php`

**GET** `https://api.aircarto.fr/sigicom/sensor-data.php?sensor_type=TYPE&serial=SERIAL&from=YYYY-MM-DD&to=YYYY-MM-DD`

En plus de `data`, `chart_data` et `period`, la réponse inclut :

- **`alias`** : texte défini via **PUT** `sensor-aliases.php` (Sigicom + `organization_id` de l’utilisateur connecté), ou `null` si aucun alias.
- **`display_name`** : identique à `alias` lorsqu’un alias existe et est non vide ; sinon concaténation `sensor_type` + espace + `serial` (même règle que les capteurs Sigicom dans les projets).

---

## 6.1 Liste des invitations

**GET** `https://api.aircarto.fr/auth/invitations-list.php`

Liste les invitations (client et admin/master) avec leur statut. Authentification requise (admin ou master).

- **Master** : reçoit toutes les invitations (toutes organisations). Paramètre optionnel `organization_id` pour filtrer par organisation.
- **Admin** : reçoit uniquement les invitations de son organisation.

Paramètres optionnels : `status` (pending, accepted, expired, revoked), `organization_id` (master uniquement), `limit` (défaut 200, max 500), `offset`.

Réponse :

```json
{
  "client_invitations": [
    {
      "id": 20,
      "email": "client@example.com",
      "suggested_username": "client_x",
      "token": "...",
      "expires_at": "2026-02-05 12:00:00",
      "status": "pending",
      "organization_id": 1,
      "created_by": 5,
      "organization_name": "Mon organisation"
    }
  ],
  "admin_invitations": [
    {
      "id": 10,
      "email": "admin@example.com",
      "suggested_username": "new_admin",
      "role": "admin",
      "token": "...",
      "expires_at": "2026-02-05 12:00:00",
      "status": "pending",
      "organization_id": 1,
      "created_by": 5,
      "created_at": "2026-02-05 10:00:00",
      "organization_name": "Mon organisation"
    }
  ]
}
```

---

## 6.2 Renvoyer un email d'invitation

**POST** `https://api.aircarto.fr/auth/invitation-resend.php`

Renvoye l’email d’invitation pour une invitation **en cours** (statut `pending`, non expirée). Authentification requise (admin ou master).

- **Master** : peut renvoyer toute invitation (client ou admin).
- **Admin** : peut renvoyer uniquement les invitations de son organisation.

Corps de la requête :

```json
{
  "type": "client",
  "id": 20
}
```

ou pour une invitation admin/master :

```json
{
  "type": "admin",
  "id": 10
}
```

Réponse (200) :

```json
{
  "message": "Invitation email resent",
  "invitation": {
    "id": 20,
    "type": "client",
    "email": "client@example.com",
    "status": "pending",
    "expires_at": "2026-02-05 12:00:00"
  }
}
```

Erreurs possibles : 400 (invitation expirée ou statut non pending), 403 (invitation d’une autre organisation pour un admin), 404 (invitation introuvable).

---

## 7. Users - Gestion des comptes

**GET** `https://api.aircarto.fr/auth/users.php`

Liste des comptes (admin = uniquement son org, master = global).

Paramètres optionnels : `role`, `organization_id` (master), `search`, `limit`, `offset`.

**PUT** `https://api.aircarto.fr/auth/users.php`

Met à jour un compte (admin = même org, master = global).

```json
{
  "id": 5,
  "email": "new@email.com",
  "full_name": "Nouveau Nom",
  "role": "admin",
  "organization_id": 1,
  "allowed_private_sensors": true,
  "uses_sigicom": false
}
```

**DELETE** `https://api.aircarto.fr/auth/users.php`

Supprime un compte (master = tous les comptes, admin = admins + clients de sa propre org).

```json
{
  "id": 5
}
```

---

## 7. Impersonate

**POST** `https://api.aircarto.fr/auth/impersonate.php`

Permet au master d’obtenir un token impersoné.

```json
{
  "target_user_id": 5,
  "reason": "Support client"
}
```

Réponse :

```json
{
  "message": "Impersonation token created",
  "impersonated_by": 1
}
```

---

## 8. Organizations

**GET** `https://api.aircarto.fr/auth/organizations.php`

Liste des organisations (master = toutes, admin = la sienne).

**POST** `https://api.aircarto.fr/auth/organizations.php`

Créer une organisation (master uniquement).

```json
{
  "name": "Nouvelle Org",
  "slug": "nouvelle-org"
}
```

Réponse :

```json
{
  "organization": {
    "id": 2,
    "name": "Nouvelle Org",
    "slug": "nouvelle-org",
    "created_at": "2026-02-05 12:00:00"
  }
}
```

**PUT** `https://api.aircarto.fr/auth/organizations.php`

Modifier une organisation (master uniquement). Body : `id` obligatoire, au moins un parmi `name` ou `slug`.

```json
{
  "id": 2,
  "name": "Nom mis à jour",
  "slug": "slug-mis-a-jour"
}
```

Réponse (200) : même format que POST avec l’organisation mise à jour. Erreurs : 404 (organisation introuvable), 409 (slug déjà utilisé par une autre organisation).

**DELETE** `https://api.aircarto.fr/auth/organizations.php`

Supprimer une organisation (master uniquement). L’organisation ne doit avoir aucun utilisateur, aucune invitation client ni invitation admin en attente.

Body :

```json
{
  "id": 2
}
```

Réponse succès (200) :

```json
{
  "message": "Organization deleted successfully"
}
```

Erreurs possibles : 404 (organisation introuvable), 409 (organisation encore utilisée : utilisateurs, invitations ou admin_invitations).

---

## 9. Sensors - Récupérer les capteurs

**GET** `https://api.aircarto.fr/auth/sensors.php`

Avec le token dans le header, tu récupères les capteurs sauvegardés de l'utilisateur.

Règles :

- Un **client** ne peut pas utiliser de capteurs avec `displaymap = NON`.
- Un **admin** peut utiliser un capteur `displaymap = NON` seulement s'il est dans **son organisation**.

Exception : un **client** peut utiliser un capteur `displaymap = NON` s'il lui a été **pré-attribué**.

Note : en cas d’invitation, les capteurs pré-attribués sont automatiquement ajoutés dans `configured_sensors` avec leur `token`.

Réponse :

```json
{
  "sensors": [
    {
      "id": "sensor_123",
      "name": "Capteur 1",
      "token": "...",
      "note": "",
      "alias": "Capteur toiture mairie",
      "display_name": "Capteur toiture mairie"
    }
  ]
}
```

Pour les rôles **client** et **admin**, chaque capteur inclut aussi **`alias`** (alias défini par l’organisation, ou `null`) et **`display_name`** (alias si défini, sinon nom du capteur). À utiliser pour l’affichage.

Si y'a rien, tu auras juste un tableau vide `[]`.

**Exemple JS :**

```javascript
const res = await fetch("https://api.aircarto.fr/auth/sensors.php", {
  credentials: "include",
});

const data = await res.json();
console.log(data.sensors);
```

---

## 9bis. Sensors - Liste globale (master/admin)

**GET** `https://api.aircarto.fr/auth/all-sensors.php`

Retourne tous les capteurs accessibles (master/admin).

- **master** : tous les capteurs
- **admin** : tous les `displaymap = OUI` + les `displaymap = NON` de son organisation  
  Requête protégée par cookie HttpOnly.

Réponse :

```json
{
  "sensors": [
    {
      "id": 12,
      "nom": "nebuleair-099",
      "capteur_type": "NebuleAir",
      "owner": "AtmoSud",
      "displaymap": "OUI"
    }
  ]
}
```

---

## 10. Sensors - Mettre à jour les capteurs

**PUT** `https://api.aircarto.fr/auth/sensors.php`

Tu envoies un tableau de capteurs :

```json
{
  "sensors": [
    {
      "id": "sensor_123",
      "name": "Capteur 1",
      "location": "Paris"
    },
    {
      "id": "sensor_456",
      "name": "Capteur 2",
      "location": "Lyon"
    }
  ]
}
```

Le format des capteurs, tu fais comme tu veux. L'API stocke juste le JSON que tu envoies.

Règles :

- Un **client** ne peut pas ajouter de capteurs avec `displaymap = NON`.
- Un **admin** peut ajouter un capteur `displaymap = NON` seulement s'il est dans **son organisation**.

Exception : un **client** peut ajouter un capteur `displaymap = NON` s'il lui a été **pré-attribué**.

Réponse :

```json
{
  "message": "Sensors updated successfully"
}
```

**Exemple JS :**

```javascript
const res = await fetch("https://api.aircarto.fr/auth/sensors.php", {
  method: "PUT",
  headers: {
    "Content-Type": "application/json",
  },
  credentials: "include",
  body: JSON.stringify({
    sensors: [{ id: "sensor_1", name: "Capteur Test", location: "Paris" }],
  }),
});
```

---

## 10bis. Assigner des capteurs à une organisation

**POST** `https://api.aircarto.fr/auth/assign-organization-sensors.php`

Assigne des capteurs à une organisation.

- **master** : peut cibler n'importe quelle organisation
- **admin** : uniquement sa propre organisation

```json
{
  "organization_id": 2,
  "sensor_ids": [12, 15]
}
```

`sensor_ids` accepte des ids **ou** des noms de capteurs.

Réponse :

```json
{
  "message": "Sensors assigned to organization",
  "organization_id": 2,
  "sensor_ids": [12, 15],
  "inserted": 2
}
```

---

## 10ter. Retirer des capteurs d’une organisation

**POST** `https://api.aircarto.fr/auth/unassign-organization-sensors.php`

Retire des capteurs d'une organisation.

- **master** : peut cibler n'importe quelle organisation
- **admin** : uniquement sa propre organisation

```json
{
  "organization_id": 2,
  "sensor_ids": [12, 15]
}
```

Réponse :

```json
{
  "message": "Sensors unassigned from organization",
  "organization_id": 2,
  "sensor_ids": [12, 15],
  "deleted": 2
}
```

---

## 10quater. Lister les capteurs d’une organisation

**GET** `https://api.aircarto.fr/auth/organization-sensors.php?organization_id=2`

Liste les capteurs associés à une organisation.

- **master** : peut cibler n'importe quelle organisation
- **admin** : uniquement sa propre organisation (paramètre ignoré)

Réponse :

```json
{
  "organization_id": 2,
  "sensors": [
    {
      "id": 497,
      "nom": "moduleair-pro001",
      "capteur_type": "ModuleAir",
      "displaymap": "OUI",
      "owner": "Ginger"
    }
  ]
}
```

---

## 10quinquies. Assigner des capteurs à un utilisateur existant

**POST** `https://api.aircarto.fr/auth/assign-user-sensors.php`

Assigne des capteurs à un utilisateur existant (ajoute dans `configured_sensors`).

- **master** : peut cibler n'importe quel utilisateur
- **admin** : uniquement les utilisateurs de sa propre organisation

```json
{
  "user_id": 123,
  "sensor_ids": [12, 15]
}
```

`sensor_ids` accepte des ids **ou** des noms de capteurs.

Règles :

- **admin** : peut assigner tous les capteurs `displaymap = OUI` (toutes orgs) et les `displaymap = NON` uniquement si le capteur est dans **son organisation**.

Réponse :

```json
{
  "message": "Sensors assigned to user",
  "user_id": 123,
  "sensor_ids": [12, 15]
}
```

---

## 11. Update Profile - Mettre à jour le profil

**PUT** `https://api.aircarto.fr/auth/update-profile.php`

Pour modifier le profil, il faut toujours passer le mot de passe actuel pour vérifier que c'est bien toi.

Tu peux modifier :

- `email`
- `full_name`
- `new_password`

Exemple pour changer juste l'email :

```json
{
  "current_password": "mon_password_actuel",
  "email": "nouvel_email@example.com"
}
```

Exemple pour changer le mot de passe :

```json
{
  "current_password": "ancien_password",
  "new_password": "nouveau_password"
}
```

Le nouveau mot de passe doit faire au moins 8 caractères.

Réponse :

```json
{
  "message": "Profile updated successfully",
  "user": {
    "id": 1,
    "username": "ton_username",
    "email": "nouvel_email@example.com",
    "full_name": "Nouveau Nom",
    "role": "client"
  }
}
```

**Exemple JS :**

```javascript
const res = await fetch("https://api.aircarto.fr/auth/update-profile.php", {
  method: "PUT",
  headers: {
    "Content-Type": "application/json",
  },
  credentials: "include",
  body: JSON.stringify({
    current_password: "mon_password_actuel",
    email: "nouvel_email@example.com",
  }),
});
```

---

## Codes de réponse

- **200** : OK
- **201** : Créé (pour register)
- **400** : Requête invalide (champs manquants, etc.)
- **401** : Pas autorisé (token manquant/invalide/expiré)
- **404** : Pas trouvé
- **409** : Conflit (username/email déjà utilisé)
- **500** : Erreur serveur

---

## Notes importantes

- CORS est ouvert et compatible cookies (origin reflété + `Access-Control-Allow-Credentials: true`)
- Origins autorisées via `CORS_ALLOWED_ORIGINS` (liste séparée par des virgules)
- Les requêtes OPTIONS sont gérées automatiquement
- Le token JWT est valide 24h
- Auth via cookie HttpOnly `aircarto_token` (pas de header Authorization)
- Tous les POST/PUT doivent avoir `Content-Type: application/json`
- Toutes les réponses sont en JSON

---

## Exemple de workflow complet

```javascript
// 1. S'inscrire
const registerRes = await fetch("https://api.aircarto.fr/auth/register.php", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  credentials: "include",
  body: JSON.stringify({
    username: "test_user",
    email: "test@example.com",
    password: "password123",
  }),
});
const registerData = await registerRes.json();

// 2. Vérifier le token
const verifyRes = await fetch("https://api.aircarto.fr/auth/verify.php", {
  credentials: "include",
});

// 3. Sauvegarder des capteurs
await fetch("https://api.aircarto.fr/auth/sensors.php", {
  method: "PUT",
  headers: {
    "Content-Type": "application/json",
  },
  credentials: "include",
  body: JSON.stringify({
    sensors: [{ id: "sensor_1", name: "Test" }],
  }),
});

// 4. Récupérer les capteurs
const sensorsRes = await fetch("https://api.aircarto.fr/auth/sensors.php", {
  credentials: "include",
});
const sensorsData = await sensorsRes.json();
console.log(sensorsData.sensors);
```

---

## Rapports personnalisés chantiers

### GET `/auth/report-data`

Génère et renvoie les données agrégées pour un rapport de monitoring chantier (données capteurs sur une période glissante).

**Accès** : admin (organisation avec slug `ginger`) ou master.

#### Paramètres de requête

| Paramètre     | Type   | Obligatoire | Description                                                        |
|---------------|--------|-------------|--------------------------------------------------------------------|
| `project_id`  | int    | oui         | Identifiant du projet                                              |
| `period_type` | string | oui         | Type de période glissante : `weekly` (7 jours) ou `monthly` (30 jours) |
| `period_end`  | string | oui         | Date de fin de période au format `YYYY-MM-DD`                      |
| `title`       | string | non         | Titre personnalisé du rapport (affiché sur la couverture)          |
| `options`     | string | non         | Objet JSON URL-encodé (ex. `dust_thresholds` pour les seuils poussières) |

Chaque génération **enregistre** le rapport en base. La réponse inclut `report_id` (identifiant pour GET/PATCH/DELETE `/auth/reports`). Pour chaque capteur **C50** sans erreur, la réponse inclut `hourly_laeq_grid`.

#### Calcul des périodes glissantes

- **`weekly`** : `date_from = period_end - 6 jours`, `date_to = period_end`
- **`monthly`** : `date_from = period_end - 29 jours`, `date_to = period_end`
- Fuseau horaire : **Europe/Paris** pour tous les calculs.

#### Réponse (200 OK)

```json
{
  "project_id": 4,
  "project_name": "SERNAM PANTIN",
  "period_type": "weekly",
  "period_end": "2026-03-07",
  "date_from": "2026-03-01",
  "date_to": "2026-03-07",
  "title": null,
  "timezone": "Europe/Paris",
  "generated_at": "2026-03-08T11:00:00+01:00",
  "sensors": [
    {
      "type": "C22",
      "alias": "Vibration MP A37",
      "serial": "105672",
      "data": { "meta": { ... }, "intervals": [ ... ] },
      "chart_data": {
        "labels": ["2026-03-01T08:00:00", "..."],
        "datasets": [
          { "label": "V", "data": [0.12, "..."], "borderColor": "#3b82f6" },
          { "label": "L", "data": [0.08, "..."], "borderColor": "#10b981" }
        ]
      },
      "latitude": 50.50588,
      "longitude": 3.12345,
      "sensor_meta": {
        "latest_calibration": "2024-01-15",
        "interval_time": 1200,
        "norm": "ICPE-Circ86 25mm/s 1-150Hz"
      }
    },
    {
      "type": "C50",
      "alias": "Acoustique",
      "serial": "116487",
      "data": { "meta": { ... }, "intervals": [ ... ] },
      "chart_data": {
        "labels": ["..."],
        "datasets": [
          { "label": "LAeq", "data": [45.2, "..."], "borderColor": "#3b82f6" },
          { "label": "LAF",  "data": [48.1, "..."], "borderColor": "#f59e0b" }
        ]
      },
      "latitude": 48.90806,
      "longitude": 2.46157,
      "sensor_meta": {
        "latest_calibration": "2024-04-11",
        "interval_time": 1200,
        "norm": "Undef"
      }
    },
    {
      "type": "NebuleAir",
      "alias": "Poussières",
      "serial": "nebuleair-pro015",
      "id": 42,
      "data": null,
      "chart_data": null,
      "dust_report": { "combined_pm": { "labels": ["2026-03-01"], "series": { "PM1": [5.2], "PM25": [12.1], "PM10": [18.0] } } },
      "latitude": 48.85,
      "longitude": 2.35
    }
  ]
}
```

#### Structure du tableau `sensors[]`

Chaque élément du tableau représente un capteur du projet. Les capteurs sont identifiés sans mention de marque tierce.

| Champ         | Type            | Description                                                                    |
|---------------|-----------------|--------------------------------------------------------------------------------|
| `type`        | string          | Type de capteur : `C22`, `C50`, `ModuleAir`, `NebuleAir`, etc.                |
| `alias`       | string          | Alias attribué au capteur (display_name)                                       |
| `serial`      | string          | Identifiant technique (numéro de série ou nom)                                 |
| `id`          | int             | ID en base de données (capteurs Aircarto uniquement)                           |
| `data`        | object \| null  | Données brutes sur la période (structure selon type, voir ci-dessous)          |
| `chart_data`  | object \| null  | Données formatées pour les graphiques (`labels` + `datasets`)                  |
| `dust_report` | object          | NebuleAir sans erreur : agrégation journalière PM + seuils / dépassements       |
| `latitude`    | float \| null   | Latitude GPS du capteur (si disponible)                                        |
| `longitude`   | float \| null   | Longitude GPS du capteur (si disponible)                                       |
| `sensor_meta` | object          | Métadonnées techniques : `latest_calibration`, `interval_time`, `norm`        |
| `error`       | string          | Présent uniquement en cas d'échec de récupération des données pour ce capteur  |

**Coordonnées** :
- Capteurs **ModuleAir / NebuleAir** : `latitude`/`longitude` depuis la base locale (`capteurs.capteurs.lat` / `.long`).
- Capteurs **C22 / C50** : `latitude`/`longitude` récupérées via l'API de gestion des mesures (matching par serial et point de mesure géolocalisé).

**Données (`data`) et graphiques (`chart_data`)** :
- Pour les capteurs C22 (vibration) et C50 (acoustique) : structure conforme à la documentation `docs/FRONT_API_SEARCH_DATA.md`. `data.intervals` contient les valeurs par créneau (V, L, T, R, rV, rL, rT pour vibration ; LAeq, LAF, LAS, L10, L50, L90 pour acoustique). **Fréquence dominante (Hz)** : l’API Sigicom ne renvoie en général pas de métrique séparée `Vf` / `Lf` / `Tf` ; elle fournit le champ **`frequency`** sur chaque entrée de vibration (ex. sur `label: "V"`). Le backend agrège ces valeurs dans `chart_data` sous les labels **`Vf`, `Lf`, `Tf`, `rVf`, `rLf`, `rTf`** (alignés sur la présentation Sigicom). `chart_data` expose `labels` (datetime) et `datasets` (un par indicateur).
- Pour les capteurs **NebuleAir** : `data` et `chart_data` restent `null` ; seul `dust_report` expose l’agrégation journalière PM et les compteurs de dépassement (seuils via `options.dust_thresholds`, défauts PM10 50/80 et PM2.5 25/10). Les capteurs **ModuleAir** et **MobileAir** ne sont pas inclus dans les rapports.

**`sensor_meta`** :
- `latest_calibration` : date de dernière calibration (string `YYYY-MM-DD`).
- `interval_time` : intervalle de mesure en secondes (int, ex. 1200 = 20 min).
- `norm` : norme applicable (string, ex. `"ICPE-Circ86 25mm/s 1-150Hz"` ou `"Undef"`).

#### Codes d'erreur

| Code | Description                                                                           |
|------|---------------------------------------------------------------------------------------|
| 401  | Non authentifié (token absent ou invalide)                                            |
| 403  | Accès interdit : rôle insuffisant, ou organisation non autorisée (slug ≠ `ginger`)    |
| 404  | Projet introuvable                                                                    |
| 422  | Paramètres invalides (`period_type` non reconnu, `period_end` mal formatée)           |
| 500  | Erreur serveur (agrégation ou base de données)                                        |

> **Note** : si un capteur individuel retourne une erreur (timeout, indisponibilité), il apparaît dans `sensors[]` avec un champ `error` et `data: null`, sans interrompre la génération du rapport pour les autres capteurs.

#### Exemple de requête

```javascript
const res = await fetch(
  "https://api.aircarto.fr/auth/report-data?project_id=4&period_type=weekly&period_end=2026-03-07",
  { credentials: "include" }
);
const report = await res.json();
// report.sensors[] → itérer pour générer le PDF
// report.report_id contient l’id du rapport enregistré (pour GET/PATCH/DELETE /auth/reports)
```

---

### GET `/auth/reports`

Liste ou re-téléchargement des rapports enregistrés.

**Accès** : admin (projets de son organisation), client (projets pour lesquels il est dans `project_user_access`), master (tous).

#### GET `?project_id=<id>` — Liste des rapports du projet

Retourne les métadonnées des rapports générés pour le projet.

**Réponse (200 OK)** : `{ "reports": [ { id, project_id, title, period_type, period_end, date_from, date_to, created_at, created_by, created_by_username } ] }`

#### GET `?id=<id>` — Détail / re-téléchargement

Retourne le contenu complet du rapport (même structure que GET `/auth/report-data`) : métadonnées + `sensors[]`. Permet de régénérer le PDF sans rappeler report-data.

**Réponse (200 OK)** : objet JSON identique au payload stocké (project_id, project_name, period_type, date_from, date_to, title, timezone, generated_at, sensors[]).

**Erreurs** : 401 (non authentifié), 403 (pas d’accès au projet), 404 (projet ou rapport introuvable), 422 (paramètre `project_id` ou `id` manquant).

---

### PATCH `/auth/reports?id=<id>`

Modifie les métadonnées d’un rapport enregistré (titre, options). Ne modifie pas la période ni le contenu (payload).

**Accès** : admin (organisation ginger) ou master.

**Body JSON** (optionnel) :

| Champ     | Type   | Description |
|-----------|--------|-------------|
| `title`   | string \| null | Nouveau titre du rapport |
| `options` | object \| null | Métadonnées optionnelles (ex. `contract_number`, `report_index`) |

**Réponse (200 OK)** : objet rapport avec id, project_id, title, period_type, period_end, date_from, date_to, created_at, created_by, options.

**Erreurs** : 400 (body invalide), 401, 403 (client ou admin sans droit sur le projet), 404 (rapport introuvable), 422 (id manquant).

---

### DELETE `/auth/reports?id=<id>`

Supprime un rapport enregistré.

**Accès** : admin (organisation ginger) ou master.

**Réponse (200 OK)** : `{ "message": "Report deleted", "id": <id> }`

**Erreurs** : 401, 403 (client ou admin sans droit sur le projet), 404 (rapport introuvable), 422 (id manquant), 500.

---

## Alertes email NebuleAir

Les alertes NebuleAir permettent de configurer des emails de dépassement de seuil par capteur, uniquement sur les particules `PM1`, `PM2.5` et `PM10`.

Le déclenchement est fait par script CLI, pas par requête HTTP :

```bash
*/5 * * * * php /var/www/api.aircarto.fr/cron/check_nebuleair_alerts.php
```

Le dossier `/cron/` est bloqué en HTTP par `.htaccess`. Le script ignore les valeurs invalides (`NULL`, sentinelles `<= -1`) et les capteurs dont `last_seen` est trop ancien. La limite de fraîcheur vaut 3600 secondes par défaut et peut être changée avec `NEBULEAIR_ALERT_MAX_SENSOR_AGE_SECONDS`.

### GET `/auth/nebuleair-alerts`

Lit la configuration d'alertes d'un capteur NebuleAir.

**Accès** : `master`, `admin` de l'organisation associée au capteur, ou `client` ayant ce capteur dans `configured_sensors`.

#### Paramètres

| Paramètre | Type | Obligatoire | Description |
|-----------|------|-------------|-------------|
| `sensor_name` | string | oui | Nom public du capteur (`nom`, ex. `nebuleair-042`) |
| `include_linked_emails` | bool | non | Ajoute la liste résolue des emails liés |

#### Réponse

```json
{
  "sensor": { "name": "nebuleair-042", "capteur_type": "NebuleAir" },
  "config": {
    "sensor_name": "nebuleair-042",
    "enabled": true,
    "notify_linked_users": true,
    "recipient_emails": ["chantier@example.com"],
    "created_by": 1,
    "created_at": "2026-05-29 11:00:00",
    "updated_at": "2026-05-29 11:00:00"
  },
  "rules": [
    {
      "id": 7,
      "sensor_name": "nebuleair-042",
      "parameter": "pm25",
      "aggregation": "hour",
      "operator": ">=",
      "threshold": 25,
      "enabled": true,
      "cooldown_minutes": 60,
      "last_triggered_at": null
    }
  ],
  "linked_emails": ["client@example.com"]
}
```

### POST `/auth/nebuleair-alerts`

Crée ou remplace la configuration et les règles du capteur. Les règles existantes du capteur sont supprimées avant insertion.

```json
{
  "sensor_name": "nebuleair-042",
  "enabled": true,
  "notify_linked_users": true,
  "recipient_emails": ["chantier@example.com"],
  "rules": [
    {
      "parameter": "pm25",
      "aggregation": "hour",
      "operator": ">=",
      "threshold": 25,
      "enabled": true,
      "cooldown_minutes": 60
    }
  ]
}
```

### PUT `/auth/nebuleair-alerts`

Met à jour la configuration. Les règles fournies sont ajoutées ou modifiées sans supprimer les autres. Si une règle contient `id`, elle est modifiée par identifiant ; sinon elle est upsert par `(sensor_name, parameter, aggregation)`.

Champs autorisés pour `parameter` : `pm1`, `pm25`, `pm10`.

Champs autorisés pour `aggregation` :
- `instant` : dernière valeur instantanée (`*_last`)
- `qh` : moyenne quart d'heure (`*_last_qh`)
- `hour` : moyenne horaire (`*_last_h`)
- `day` : moyenne journalière (`*_last_d`)

### DELETE `/auth/nebuleair-alerts`

Supprime une règle ou toute la configuration d'un capteur.

```json
{ "rule_id": 7 }
```

ou :

```json
{ "sensor_name": "nebuleair-042" }
```

**Erreurs** : 400 (JSON ou règle invalide), 401 (non authentifié), 403 (pas accès au capteur), 404 (capteur/règle introuvable), 500.
