# IliaCloud - Partage d'acces multi-user

## Objet du document

Ce document decrit le modele technique complet de la feature de partage d'acces tel qu'il doit etre implemente pour IliaCloud, en partant du code reel et du schema SQL reel du depot.

Tout ce qui est affirme ci-dessous est base sur des elements verifiables dans le code existant :

- Schema SQL reel : [backend/src/db/schema.sql](backend/src/db/schema.sql)
- Migration auto au demarrage : [backend/src/db/autoMigrate.js](backend/src/db/autoMigrate.js), [backend/src/index.js](backend/src/index.js)
- Auth actuelle : [backend/src/middleware/auth.js](backend/src/middleware/auth.js)
- Quotas et plans : [backend/src/middleware/planLimits.js](backend/src/middleware/planLimits.js), [backend/src/config/plans.js](backend/src/config/plans.js)
- Audit actuel : [backend/src/middleware/audit.js](backend/src/middleware/audit.js)
- Ownership serveur actuel : [backend/src/helpers/serverOwnership.js](backend/src/helpers/serverOwnership.js)
- Helpers de suppression scopes `user_id` et `server_id` : [backend/src/helpers/routeHelpers.js](backend/src/helpers/routeHelpers.js)
- Routes backend reelles : [backend/src/index.js](backend/src/index.js)
- Contexte frontend actuel base sur le serveur actif : [frontend/src/contexts/ServerContext.jsx](frontend/src/contexts/ServerContext.jsx)
- Routage frontend actuel : [frontend/src/App.jsx](frontend/src/App.jsx)
- Parametres frontend actuels : [frontend/src/pages/SettingsPage.jsx](frontend/src/pages/SettingsPage.jsx)

Important : ce document s'appuie sur les donnees reelles de structure de la base et du code. Il ne s'appuie pas sur un dump de donnees de production, car aucun acces direct a la base de prod n'est utilise ici.

---

## Modele produit cible

Le modele cible exact est le suivant :

1. Un serveur est un support technique.
2. Un meme serveur peut heberger plusieurs projets.
3. Un groupe est le perimetre metier visible.
4. Un groupe peut contenir des ressources provenant d'un ou plusieurs serveurs.
5. Un membre ne voit que les ressources des groupes auxquels il a acces.
6. Les droits ne sont pas de simples roles figes.
7. Pour chaque membre et pour chaque zone fonctionnelle d'un groupe, le proprietaire choisit un niveau d'acces.

Formulation fonctionnelle exacte de la feature :

- creer des groupes de projets
- rattacher des ressources de un ou plusieurs serveurs a un groupe
- inviter des utilisateurs dans un groupe
- definir, pour chaque utilisateur et pour chaque zone du groupe, aucun acces, lecture seule ou acces complet
- tracer dans l'audit qui a fait quoi, dans quel groupe

Exemples de groupes :

- CoolCare
- FailDaily
- RollerLogic

Exemple de realite technique cible :

- un meme VPS peut contenir des containers Docker de CoolCare et de FailDaily
- dans le groupe CoolCare, on ne doit voir que les containers, logs, actions, fichiers, backups, alertes et ressources CoolCare
- un technicien ayant acces a CoolCare ne doit jamais voir FailDaily ni RollerLogic

---

## Etat actuel verifiable du code

### 1. Auth et identite

Le middleware [backend/src/middleware/auth.js](backend/src/middleware/auth.js) injecte aujourd'hui :

```js
req.user = { id: payload.sub, email: payload.email, plan: payload.plan };
```

Il n'existe actuellement ni contexte de groupe, ni contexte de membre, ni niveau d'acces par zone.

### 2. Modele de donnees actuel

Le schema actuel est majoritairement organise autour de deux axes :

- `user_id`
- `server_id`

Tables reelles actuelles directement concernees par le futur partage d'acces :

- `servers` : [backend/src/db/schema.sql](backend/src/db/schema.sql)
- `log_paths` : [backend/src/db/schema.sql](backend/src/db/schema.sql)
- `quick_actions` : [backend/src/db/schema.sql](backend/src/db/schema.sql)
- `chat_sessions` : [backend/src/db/schema.sql](backend/src/db/schema.sql)
- `metrics_history` : [backend/src/db/schema.sql](backend/src/db/schema.sql)
- `alert_rules` : [backend/src/db/schema.sql](backend/src/db/schema.sql)
- `alert_history` : [backend/src/db/schema.sql](backend/src/db/schema.sql)
- `uptime_monitors` : [backend/src/db/schema.sql](backend/src/db/schema.sql)
- `uptime_history` : [backend/src/db/schema.sql](backend/src/db/schema.sql)
- `backups` : [backend/src/db/schema.sql](backend/src/db/schema.sql)
- `backup_schedules` : [backend/src/db/schema.sql](backend/src/db/schema.sql)
- `webhooks` : [backend/src/db/schema.sql](backend/src/db/schema.sql)
- `ssl_certificates` : [backend/src/db/schema.sql](backend/src/db/schema.sql)
- `status_pages` et `status_page_monitors` : [backend/src/db/schema.sql](backend/src/db/schema.sql)
- `audit_logs` : [backend/src/db/schema.sql](backend/src/db/schema.sql)
- `annotations` : [backend/src/db/schema.sql](backend/src/db/schema.sql)
- `subscriptions` : [backend/src/db/schema.sql](backend/src/db/schema.sql)

Points structurels verifies :

- `servers` appartient a un `user_id`
- `log_paths`, `quick_actions`, `metrics_history`, `backups`, `backup_schedules`, `annotations` et `alert_rules` sont relies a `server_id`
- `uptime_monitors`, `webhooks`, `ssl_certificates`, `status_pages` sont scopes par `user_id`
- `chat_sessions` est scope par `user_id`, avec `server_id` facultatif
- `audit_logs` n'a aujourd'hui ni `group_id`, ni `owner_user_id`, ni `resource_type`, ni `resource_id`

### 3. Routes backend actuelles

Les routes sont montees dans [backend/src/index.js](backend/src/index.js).

Routes compte/utilisateur actuelles :

- `/auth`
- `/ssh-keys`
- `/servers`
- `/api-keys`
- `/user-api-keys`
- `/chat`
- `/alerts`
- `/uptime`
- `/webhooks`
- `/ssl`
- `/status-pages`
- `/audit`
- `/config`
- `/billing`
- `/digest`
- `/report`
- `/api/v1`

Routes imbriquees par serveur actuelles :

- `/servers/:serverId/dashboard`
- `/servers/:serverId/docker`
- `/servers/:serverId/logs`
- `/servers/:serverId/quick-actions`
- `/servers/:serverId/metrics`
- `/servers/:serverId/annotations`
- `/servers/:serverId/backups`
- `/servers/:serverId/files`
- `/servers/:serverId/cron`

### 4. Mecanismes de controle d'acces actuels

Il y a aujourd'hui trois modes de controle d'acces verifies dans le code :

#### A. Filtrage direct sur `user_id`

Exemples reels :

- [backend/src/routes/servers.js](backend/src/routes/servers.js)
- [backend/src/routes/webhooks.js](backend/src/routes/webhooks.js)
- [backend/src/routes/uptime.js](backend/src/routes/uptime.js)
- [backend/src/routes/statusPage.js](backend/src/routes/statusPage.js)
- [backend/src/routes/chat.js](backend/src/routes/chat.js)

Pattern actuel :

```sql
WHERE user_id = $1
```

#### B. Verification de propriete serveur via helper

Helper actuel : [backend/src/helpers/serverOwnership.js](backend/src/helpers/serverOwnership.js)

```js
async function ownsServer(serverId, userId) {
  const { rows } = await db.query(
    'SELECT id FROM servers WHERE id = $1 AND user_id = $2',
    [serverId, userId],
  );
  return rows.length > 0;
}
```

Utilise notamment dans :

- [backend/src/routes/logs.js](backend/src/routes/logs.js)
- [backend/src/routes/quickActions.js](backend/src/routes/quickActions.js)
- [backend/src/routes/alerts.js](backend/src/routes/alerts.js)
- [backend/src/helpers/routeHelpers.js](backend/src/helpers/routeHelpers.js)

#### C. Verification de propriete serveur recodee localement

Certaines routes ne reutilisent meme pas le helper commun et reimplementent leur propre `ownsServer`.

Cas verifies :

- [backend/src/routes/files.js](backend/src/routes/files.js)
- [backend/src/routes/backups.js](backend/src/routes/backups.js)
- [backend/src/routes/cron.js](backend/src/routes/cron.js)
- [backend/src/websocket.js](backend/src/websocket.js)

Conclusion verifiable : le controle d'acces n'est aujourd'hui ni centralise, ni groupe-aware.

### 5. Quotas plans actuels

Les limites reelles de plan sont definies dans [backend/src/config/plans.js](backend/src/config/plans.js).

Point important deja en place et directement reutilisable :

- `business.users = 3`
- `enterprise.users = Infinity`

Le middleware actuel [backend/src/middleware/planLimits.js](backend/src/middleware/planLimits.js) sait compter des ressources par `user_id`, mais ne sait pas encore compter des membres de groupes ni des invitations.

### 6. Frontend actuel

Le frontend est aujourd'hui pilote par un contexte serveur : [frontend/src/contexts/ServerContext.jsx](frontend/src/contexts/ServerContext.jsx)

Responsabilites reelles de `ServerContext` :

- charger `/servers`
- charger `/servers/overview`
- memoriser `activeServerId` dans `localStorage`
- servir une vue `Tous les serveurs`

Le routage frontend actuel est centre sur les pages fonctionnelles globales : [frontend/src/App.jsx](frontend/src/App.jsx)

Pages impactees par le futur group scope :

- [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx)
- [frontend/src/pages/DockerPage.jsx](frontend/src/pages/DockerPage.jsx)
- [frontend/src/pages/LogsPage.jsx](frontend/src/pages/LogsPage.jsx)
- [frontend/src/pages/ActionsPage.jsx](frontend/src/pages/ActionsPage.jsx)
- [frontend/src/pages/ChatPage.jsx](frontend/src/pages/ChatPage.jsx)
- [frontend/src/pages/BackupPage.jsx](frontend/src/pages/BackupPage.jsx)
- [frontend/src/pages/FilesPage.jsx](frontend/src/pages/FilesPage.jsx)
- [frontend/src/pages/CronPage.jsx](frontend/src/pages/CronPage.jsx)
- [frontend/src/pages/UptimePage.jsx](frontend/src/pages/UptimePage.jsx)
- [frontend/src/pages/WebhooksPage.jsx](frontend/src/pages/WebhooksPage.jsx)
- [frontend/src/pages/SSLPage.jsx](frontend/src/pages/SSLPage.jsx)
- [frontend/src/pages/StatusPagesPage.jsx](frontend/src/pages/StatusPagesPage.jsx)
- [frontend/src/pages/AuditPage.jsx](frontend/src/pages/AuditPage.jsx)
- [frontend/src/pages/SettingsPage.jsx](frontend/src/pages/SettingsPage.jsx)

Conclusion verifiable : l'application n'a aujourd'hui aucun `GroupContext`, aucun selecteur de groupe et aucun filtrage d'UI par groupe.

---

## Verite technique non negociable issue du code actuel

Cette section est critique. Elle ne donne pas des options produit. Elle liste les contraintes reelles que le code actuel impose si l'on veut tenir la promesse "un membre d'un groupe ne voit que ce groupe".

### 1. Un serveur ne peut pas etre rattache a un seul groupe

Dans ton modele, c'est faux fonctionnellement.

Pourquoi :

- un meme VPS peut heberger plusieurs projets
- [backend/src/routes/docker.js](backend/src/routes/docker.js) liste tous les containers du serveur
- [backend/src/routes/files.js](backend/src/routes/files.js) permet de lire n'importe quel chemin valide du serveur
- [backend/src/routes/cron.js](backend/src/routes/cron.js) lit la crontab brute du serveur

Donc la bonne relation n'est pas `server -> group`.

La bonne relation est :

- `group -> resources`
- certaines ressources vivent sur `server_id`

### 2. Les metriques CPU/RAM/disque/load actuelles sont serveur-globales

Verification : [backend/src/routes/servers.js](backend/src/routes/servers.js), [backend/src/routes/dashboard.js](backend/src/routes/dashboard.js), [backend/src/routes/metrics.js](backend/src/routes/metrics.js), [backend/src/websocket.js](backend/src/websocket.js)

Les collectes actuelles lisent la machine dans son ensemble :

- `top`
- `free -m`
- `df -h`
- `/proc/loadavg`

Consequence technique non evitable :

- sur un VPS partage entre CoolCare et FailDaily, les metriques du serveur melangent les deux
- il est impossible, avec le code actuel, de garantir des metriques "par groupe" sans changer radicalement le collecteur

Ce que cela implique pour la feature :

- soit le groupe affiche une vue "sante du serveur support" en assumant que c'est global au serveur
- soit les metriques serveur-globales sont reservees au proprietaire
- soit on ajoute plus tard une vraie couche de metriques applicatives ou container-level

### 3. Le terminal SSH actuel n'est pas isolable par groupe

Verification : [backend/src/websocket.js](backend/src/websocket.js)

Le terminal ouvre une vraie session SSH sur le serveur. Il ne connait aujourd'hui que :

- `userId`
- `serverId`

Consequence technique non evitable :

- si un membre a un terminal complet sur un serveur partage, il peut potentiellement voir tout le serveur
- donc potentiellement tous les projets presents sur ce serveur

Conclusion technique :

- `terminal = complet` casse l'isolation stricte d'un groupe sur serveur partage
- si le terminal existe dans la feature, le document d'implementation doit l'assumer explicitement

### 4. Le navigateur de fichiers actuel n'est pas isolable sans allowlist

Verification : [backend/src/routes/files.js](backend/src/routes/files.js)

Le code actuel :

- accepte n'importe quel `path` valide
- browse, read et write sur ce path
- ne lie ce path a aucune notion de projet

Consequence technique :

- sans table d'allowlist des repertoires autorises par groupe, un membre pourrait lire ou ecrire hors de son projet

### 5. Le Cron actuel n'est pas isolable par groupe dans son mode brut

Verification : [backend/src/routes/cron.js](backend/src/routes/cron.js)

Le code actuel :

- lit `crontab -l`
- identifie les taches par index de ligne
- modifie et supprime par index

Consequence technique :

- sur un serveur partage, la crontab brute ne porte aucune information de groupe
- on ne peut pas dire de maniere fiable quelles lignes appartiennent a CoolCare et lesquelles appartiennent a FailDaily

Conclusion technique :

- le mode Cron groupe-aware doit passer par des entrees IliaCloud taguees ou gerees en base
- l'index brut de crontab n'est pas compatible avec une isolation stricte par groupe

### 6. Docker actuel n'est pas groupe-aware

Verification : [backend/src/routes/docker.js](backend/src/routes/docker.js)

Le code actuel :

- fait `docker ps -a`
- liste tous les containers
- fait des actions `start/stop/restart` sur tout container autorise par nom

Consequence technique :

- sans table de ciblage Docker par groupe, un membre d'un groupe verrait tout le serveur

### 7. Le scan de logs actuel n'est pas groupe-aware

Verification : [backend/src/routes/logs.js](backend/src/routes/logs.js)

Le code actuel :

- scanne des chemins systeme standards
- liste tous les containers Docker du serveur

Consequence technique :

- sur serveur partage, le scan auto revele des ressources d'autres projets

### 8. Le chat actuel n'est pas groupe-aware

Verification : [backend/src/routes/chat.js](backend/src/routes/chat.js)

Le code actuel :

- rattache la session a `user_id` et eventuellement `server_id`
- n'a aucun `group_id`
- laisse l'IA agir avec des tools sur `serverId`

Consequence technique :

- sans contexte de groupe injecte dans le systeme d'outils, le chat agit au niveau serveur complet

---

## Modele cible exact a implementer

### 1. Principe de base

Le serveur reste un support technique.

Le groupe devient le perimetre fonctionnel visible.

Les ressources d'un groupe peuvent vivre :

- sur un serveur
- sur plusieurs serveurs
- sans serveur direct, comme uptime, SSL, webhooks, status pages

### 2. Relations exactes

- un owner peut creer plusieurs groupes
- un groupe peut inclure plusieurs serveurs supports
- un meme serveur peut etre utilise par plusieurs groupes
- un groupe contient ses propres ressources explicites
- un membre est invite dans un groupe, pas sur un serveur
- les permissions sont definies par membre et par zone dans le groupe

### 3. Niveaux d'acces exacts

Les niveaux d'acces standards sont :

- `none`
- `read`
- `write`

Table de niveaux par zone :

| Zone | Niveaux | Sens exact |
| --- | --- | --- |
| dashboard | none, read | Voir les cartes de sante et vues de synthese |
| metrics | none, read | Voir l'historique et le temps reel |
| docker | none, read, write | Voir les containers ou agir dessus |
| logs | none, read | Lister, tailer, chercher plus tard |
| files | none, read, write | Browse, read, write dans les racines autorisees |
| terminal | none, write | Ouvrir un shell SSH brut |
| quick_actions | none, read, write | Voir, creer, modifier, executer |
| cron | none, read, write | Voir et gerer uniquement les entrees IliaCloud scopees au groupe |
| backups | none, read, write | Voir, lancer, restaurer, supprimer |
| chat | none, read, write | Voir les sessions, envoyer des messages, utiliser les tools |
| alerts | none, read, write | Voir et configurer les alertes |
| annotations | none, read, write | Voir, creer, modifier, supprimer |
| uptime | none, read, write | Voir, creer, modifier, supprimer les monitors du groupe |
| webhooks | none, read, write | Voir, creer, modifier, supprimer les webhooks du groupe |
| ssl | none, read, write | Voir, creer, modifier, supprimer les certificats du groupe |
| status_pages | none, read, write | Voir, creer, modifier, supprimer les status pages du groupe |
| audit | none, read | Voir l'audit filtre au groupe |

Ressources owner-only, hors partage :

- billing
- abonnement Stripe
- compte utilisateur
- suppression de compte
- changement d'email
- 2FA du compte
- cles SSH privees
- cles API IA du compte
- user API keys du compte
- export/import global du compte

### 4. JSON de permissions cible

Forme recommandee et exploitable directement en backend :

```json
{
  "dashboard": "read",
  "metrics": "read",
  "docker": "write",
  "logs": "read",
  "files": "write",
  "terminal": "none",
  "quick_actions": "write",
  "cron": "read",
  "backups": "write",
  "chat": "write",
  "alerts": "read",
  "annotations": "write",
  "uptime": "write",
  "webhooks": "read",
  "ssl": "read",
  "status_pages": "read",
  "audit": "read"
}
```

---

## Schema SQL cible

### 1. Nouvelles tables coeur

#### `access_groups`

```sql
CREATE TABLE IF NOT EXISTS access_groups (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  owner_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  name          TEXT NOT NULL,
  slug          TEXT NOT NULL,
  description   TEXT,
  color         TEXT,
  deleted_at    TIMESTAMPTZ,
  created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at    TIMESTAMPTZ,
  UNIQUE(owner_user_id, slug)
);

CREATE INDEX IF NOT EXISTS idx_access_groups_owner ON access_groups(owner_user_id);
```

Raison : le groupe est l'entite metier principale visible dans l'application.

#### `access_group_members`

```sql
CREATE OR REPLACE FUNCTION is_valid_group_permissions(payload JSONB)
RETURNS BOOLEAN
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
  key TEXT;
  value TEXT;
  allowed_keys TEXT[] := ARRAY[
    'dashboard','metrics','docker','logs','files','terminal','quick_actions',
    'cron','backups','chat','alerts','annotations','uptime','webhooks',
    'ssl','status_pages','audit'
  ];
BEGIN
  IF jsonb_typeof(payload) <> 'object' THEN
    RETURN FALSE;
  END IF;

  FOR key, value IN
    SELECT e.key, e.value #>> '{}'
    FROM jsonb_each(payload) AS e
  LOOP
    IF NOT (key = ANY(allowed_keys)) THEN
      RETURN FALSE;
    END IF;
    IF value NOT IN ('none', 'read', 'write') THEN
      RETURN FALSE;
    END IF;
  END LOOP;

  RETURN TRUE;
END;
$$;

CREATE TABLE IF NOT EXISTS access_group_members (
  id                 UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  group_id           UUID NOT NULL REFERENCES access_groups(id) ON DELETE RESTRICT,
  user_id            UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  invited_by_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  status             TEXT NOT NULL DEFAULT 'active'
                    CHECK (status IN ('active', 'revoked')),
  permissions        JSONB NOT NULL DEFAULT '{}'::jsonb
                    CHECK (is_valid_group_permissions(permissions)),
  joined_at          TIMESTAMPTZ,
  created_at         TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at         TIMESTAMPTZ,
  UNIQUE(group_id, user_id)
);

CREATE INDEX IF NOT EXISTS idx_access_group_members_group ON access_group_members(group_id);
CREATE INDEX IF NOT EXISTS idx_access_group_members_user ON access_group_members(user_id);
```

Raison : les droits sont portes par le couple `group + member`, pas par le serveur.

#### `access_group_invitations`

```sql
CREATE TABLE IF NOT EXISTS access_group_invitations (
  id                 UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  group_id           UUID NOT NULL REFERENCES access_groups(id) ON DELETE RESTRICT,
  invited_by_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  email              TEXT NOT NULL,
  token_hash         TEXT NOT NULL UNIQUE,
  permissions        JSONB NOT NULL DEFAULT '{}'::jsonb
                    CHECK (is_valid_group_permissions(permissions)),
  send_count         INTEGER NOT NULL DEFAULT 1 CHECK (send_count >= 1 AND send_count <= 5),
  expires_at         TIMESTAMPTZ NOT NULL,
  accepted_at        TIMESTAMPTZ,
  revoked_at         TIMESTAMPTZ,
  linked_user_id     UUID REFERENCES users(id) ON DELETE SET NULL,
  created_at         TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_access_group_invitations_group ON access_group_invitations(group_id);
CREATE INDEX IF NOT EXISTS idx_access_group_invitations_email ON access_group_invitations(email);
CREATE UNIQUE INDEX IF NOT EXISTS uniq_active_group_invitation_email
ON access_group_invitations(group_id, email)
WHERE accepted_at IS NULL AND revoked_at IS NULL;
```

Raison : une invitation doit exister avant que le compte membre n'accepte l'acces.

Flux d'invitation cible :

1. l'owner invite un email dans un groupe
2. si l'email correspond deja a un compte existant, le mail contient un lien d'acceptation direct
3. si l'email ne correspond a aucun compte, le mail redirige vers l'inscription
4. apres creation du compte, le backend verifie les invitations en attente non expirees pour cet email
5. si une invitation valide existe, le compte cree est rattache automatiquement au groupe et `linked_user_id` est renseigne
6. `accepted_at` est renseigne uniquement au moment de l'acceptation effective

Consequence :

- la liaison post-inscription n'est pas un comportement optionnel, elle fait partie du flux normal de la feature
- la verification des invitations en attente doit etre branchee dans le flux d'inscription et dans un endpoint d'acceptation explicite

Important sur l'unicite des invitations :

- PostgreSQL ne peut pas exprimer proprement en index partiel la notion "non expiree selon NOW()"
- l'index ci-dessus empeche plusieurs invitations simultanees non acceptees/non revoquees pour le meme couple `(group_id, email)`
- la regle "ne pas renvoyer tant qu'une invitation non expiree existe" doit etre enforcee en middleware transactionnel en verifiant `expires_at > NOW()`
- la regle "blocage definitif apres 5 envois cumules" doit aussi etre enforcee en transaction applicative a partir de `send_count`

Modele retenu pour `send_count` :

- le renvoi d'invitation ne cree pas une nouvelle ligne si une ligne existe deja pour `(group_id, email)`
- le renvoi met a jour la ligne existante :
  - nouveau `token_hash`
  - `expires_at` reinitialise
  - `revoked_at` remis a `NULL`
  - `accepted_at` reste `NULL`
  - `send_count = send_count + 1`
- quand `send_count >= 5`, tout nouvel envoi est refuse de maniere definitive pour ce couple `(group_id, email)`

Ce choix supprime l'ambiguite "cumul cross-lignes" et rend la regle verifiable directement sur une ligne unique.

#### `access_group_servers`

```sql
CREATE TABLE IF NOT EXISTS access_group_servers (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  group_id    UUID NOT NULL REFERENCES access_groups(id) ON DELETE RESTRICT,
  server_id   UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE(group_id, server_id)
);

CREATE INDEX IF NOT EXISTS idx_access_group_servers_group ON access_group_servers(group_id);
CREATE INDEX IF NOT EXISTS idx_access_group_servers_server ON access_group_servers(server_id);
```

Raison : un groupe peut utiliser un ou plusieurs serveurs, et un serveur peut servir plusieurs groupes.

### 2. Nouvelles tables pour les ressources non directement modelisees aujourd'hui

#### `access_group_docker_targets`

```sql
CREATE TABLE IF NOT EXISTS access_group_docker_targets (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  group_id      UUID NOT NULL REFERENCES access_groups(id) ON DELETE RESTRICT,
  server_id     UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
  target_type   TEXT NOT NULL CHECK (target_type IN ('container', 'service', 'compose_project', 'stack')),
  match_method  TEXT NOT NULL DEFAULT 'name' CHECK (match_method IN ('name', 'label')),
  selector      TEXT NOT NULL,
  label         TEXT,
  created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE(group_id, server_id, target_type, match_method, selector)
);

CREATE INDEX IF NOT EXISTS idx_access_group_docker_targets_group ON access_group_docker_targets(group_id);
CREATE INDEX IF NOT EXISTS idx_access_group_docker_targets_server ON access_group_docker_targets(server_id);
```

Raison : Docker est aujourd'hui lu en direct via SSH et ne possede aucune colonne `group_id` dans l'etat actuel du projet.

Strategie de matching Docker :

- `match_method = 'name'` : le `selector` est compare au nom du container/service/stack (matching exact)
- `match_method = 'label'` : le `selector` est un label Docker a verifier sur le container (ex: `com.docker.compose.project=coolcare`)
- le matching par label est recommande pour les environnements multi-projets car les noms de containers peuvent changer apres recreation ou scaling
- labels Docker courants exploitables : `com.docker.compose.project`, `com.docker.stack.namespace`, ou labels personnalises poses par l'owner
- le backend doit lire les labels du container via `docker inspect` ou `docker ps --format` et filtrer selon `match_method`

#### `access_group_file_roots`

```sql
CREATE TABLE IF NOT EXISTS access_group_file_roots (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  group_id    UUID NOT NULL REFERENCES access_groups(id) ON DELETE RESTRICT,
  server_id   UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
  label       TEXT NOT NULL,
  root_path   TEXT NOT NULL,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE(group_id, server_id, root_path)
);

CREATE INDEX IF NOT EXISTS idx_access_group_file_roots_group ON access_group_file_roots(group_id);
CREATE INDEX IF NOT EXISTS idx_access_group_file_roots_server ON access_group_file_roots(server_id);
```

Raison : [backend/src/routes/files.js](backend/src/routes/files.js) autorise aujourd'hui n'importe quel chemin valide du serveur.

#### `access_group_cron_entries`

```sql
CREATE TABLE IF NOT EXISTS access_group_cron_entries (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  group_id    UUID NOT NULL REFERENCES access_groups(id) ON DELETE RESTRICT,
  server_id   UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
  target_user TEXT NOT NULL DEFAULT 'user' CHECK (target_user IN ('user', 'root')),
  schedule    TEXT NOT NULL,
  command     TEXT NOT NULL,
  enabled     BOOLEAN NOT NULL DEFAULT TRUE,
  marker      TEXT NOT NULL UNIQUE,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at  TIMESTAMPTZ
);

CREATE INDEX IF NOT EXISTS idx_access_group_cron_entries_group ON access_group_cron_entries(group_id);
CREATE INDEX IF NOT EXISTS idx_access_group_cron_entries_server ON access_group_cron_entries(server_id);
```

Raison : [backend/src/routes/cron.js](backend/src/routes/cron.js) manipule actuellement la crontab par index de ligne, ce qui est incompatible avec le partage multi-projets sur serveur partage.

### 3. Colonnes a ajouter aux tables existantes

Colonnes `group_id` a ajouter :

```sql
-- metrics_history ne recoit pas de group_id volontairement (metriques machine globales au serveur)

ALTER TABLE log_paths         ADD COLUMN IF NOT EXISTS group_id UUID REFERENCES access_groups(id) ON DELETE SET NULL;
ALTER TABLE quick_actions     ADD COLUMN IF NOT EXISTS group_id UUID REFERENCES access_groups(id) ON DELETE SET NULL;
ALTER TABLE chat_sessions     ADD COLUMN IF NOT EXISTS group_id UUID REFERENCES access_groups(id) ON DELETE SET NULL;
ALTER TABLE alert_rules       ADD COLUMN IF NOT EXISTS group_id UUID REFERENCES access_groups(id) ON DELETE SET NULL;
ALTER TABLE alert_history     ADD COLUMN IF NOT EXISTS group_id UUID REFERENCES access_groups(id) ON DELETE SET NULL;
ALTER TABLE backups           ADD COLUMN IF NOT EXISTS group_id UUID REFERENCES access_groups(id) ON DELETE SET NULL;
ALTER TABLE backup_schedules  ADD COLUMN IF NOT EXISTS group_id UUID REFERENCES access_groups(id) ON DELETE SET NULL;
ALTER TABLE uptime_monitors   ADD COLUMN IF NOT EXISTS group_id UUID REFERENCES access_groups(id) ON DELETE SET NULL;
ALTER TABLE webhooks          ADD COLUMN IF NOT EXISTS group_id UUID REFERENCES access_groups(id) ON DELETE SET NULL;
ALTER TABLE ssl_certificates  ADD COLUMN IF NOT EXISTS group_id UUID REFERENCES access_groups(id) ON DELETE SET NULL;
ALTER TABLE status_pages      ADD COLUMN IF NOT EXISTS group_id UUID REFERENCES access_groups(id) ON DELETE SET NULL;
ALTER TABLE annotations       ADD COLUMN IF NOT EXISTS group_id UUID REFERENCES access_groups(id) ON DELETE SET NULL;
```

Indexes a ajouter :

```sql
CREATE INDEX IF NOT EXISTS idx_log_paths_group ON log_paths(group_id);
CREATE INDEX IF NOT EXISTS idx_quick_actions_group ON quick_actions(group_id);
CREATE INDEX IF NOT EXISTS idx_chat_sessions_group ON chat_sessions(group_id);
CREATE INDEX IF NOT EXISTS idx_alert_rules_group ON alert_rules(group_id);
CREATE INDEX IF NOT EXISTS idx_alert_history_group_time ON alert_history(group_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_backups_group_time ON backups(group_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_backup_schedules_group ON backup_schedules(group_id);
CREATE INDEX IF NOT EXISTS idx_uptime_monitors_group ON uptime_monitors(group_id);
CREATE INDEX IF NOT EXISTS idx_webhooks_group ON webhooks(group_id);
CREATE INDEX IF NOT EXISTS idx_ssl_certificates_group ON ssl_certificates(group_id);
CREATE INDEX IF NOT EXISTS idx_status_pages_group ON status_pages(group_id);
CREATE INDEX IF NOT EXISTS idx_annotations_group_time ON annotations(group_id, timestamp DESC);
```

Point critique sur les tables actuellement scopees par `user_id` seul :

Les tables `uptime_monitors`, `webhooks`, `ssl_certificates` et `status_pages` sont aujourd'hui filtrees exclusivement par `user_id` dans les routes. Avec l'ajout de `group_id`, l'axe de possession change :

- avant : `WHERE user_id = $1` (l'utilisateur connecte est le proprietaire)
- apres : `WHERE group_id = $1` (le groupe actif determine la visibilite)

Consequence pour les routes :

- un membre qui cree un monitor uptime dans un groupe ne doit pas renseigner son propre `user_id` comme proprietaire — c'est le `owner_user_id` du groupe qui reste le proprietaire reel de la ressource
- les queries de creation doivent passer de `INSERT INTO ... (user_id, ...) VALUES (req.user.id, ...)` a `INSERT INTO ... (user_id, group_id, ...) VALUES (req.access.ownerUserId, req.access.groupId, ...)`
- les queries de lecture doivent passer de `WHERE user_id = req.user.id` a `WHERE group_id = req.access.groupId`
- la colonne `user_id` existante reste renseignee (avec le `owner_user_id`) pour ne pas casser les contraintes FK et les queries legacy en mode global owner

Tests explicites a ecrire pour cette transition :

- un membre cree un monitor uptime dans un groupe : `user_id` = owner, `group_id` = groupe actif
- l'owner en mode global voit tous les monitors (toutes valeurs de `group_id`)
- l'owner en mode groupe ne voit que ceux du groupe actif
- un membre d'un autre groupe ne voit pas les monitors d'un groupe auquel il n'appartient pas

### 4. Evolution de l'audit

Schema cible recommande pour `audit_logs` :

```sql
ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS owner_user_id UUID REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS group_id UUID REFERENCES access_groups(id) ON DELETE SET NULL;
ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS resource_type TEXT;
ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS resource_id UUID;
```

Raison : l'audit actuel, visible dans [backend/src/middleware/audit.js](backend/src/middleware/audit.js), ne stocke que `user_id`, `action`, `category`, `target`, `details`, `ip`.

Pour la future feature, il faut pouvoir repondre a des questions precises :

- quel membre a fait l'action
- pour quel owner
- dans quel groupe
- sur quel type de ressource
- sur quelle ressource exacte

### 5. Politique de suppression des groupes

Pour eviter toute perte de donnees accidentelle, la suppression de groupe doit etre une suppression logique.

Regles cibles :

- pas de `DELETE` physique en V1 pour `access_groups`
- soft-delete via `deleted_at` sur `access_groups`
- les ressources conservent leurs lignes avec `group_id` (ou passent a `NULL` en cas d'operation admin de purge)
- operation de purge physique reservee a un job admin explicite, jamais depuis l'UI courante

Consequence :

- on evite le scenario "clic de suppression groupe = suppression irreversible des backups, alertes, monitors"
- on garde une possibilite de restauration

---

## Mapping exact entre ressources actuelles et futur scope de groupe

| Zone | Etat actuel verifiable | Probleme actuel | Cible groupe-aware |
| --- | --- | --- | --- |
| Servers | `servers.user_id` + [backend/src/routes/servers.js](backend/src/routes/servers.js) | pas de groupe | `access_group_servers` + filtrage par groupe actif |
| Dashboard | [backend/src/routes/servers.js](backend/src/routes/servers.js), [backend/src/routes/dashboard.js](backend/src/routes/dashboard.js) | metriques serveur-globales | visible au groupe, mais signal global serveur |
| Metrics | `metrics_history.server_id` + [backend/src/routes/metrics.js](backend/src/routes/metrics.js) | pas de groupe, signal global serveur | filtre par serveur du groupe, avec meme limite de globalite |
| Docker | [backend/src/routes/docker.js](backend/src/routes/docker.js) | `docker ps -a` expose tout le serveur | `access_group_docker_targets` + filtrage par selecteur |
| Logs | `log_paths.server_id` + [backend/src/routes/logs.js](backend/src/routes/logs.js) | scan auto expose tous les logs possibles | `log_paths.group_id` + scan filtre aux ressources du groupe |
| Files | [backend/src/routes/files.js](backend/src/routes/files.js) | n'importe quel path valide | `access_group_file_roots` + validation stricte du chemin inclus |
| Terminal | [backend/src/websocket.js](backend/src/websocket.js) | shell brut, non isolable | permission explicite avec avertissement structurel |
| Quick actions | `quick_actions.server_id` + [backend/src/routes/quickActions.js](backend/src/routes/quickActions.js) | pas de groupe | `quick_actions.group_id` |
| Cron | [backend/src/routes/cron.js](backend/src/routes/cron.js) | crontab par index, non regroupable | `access_group_cron_entries` + sync serveur via marker |
| Backups | `backups.server_id`, `backup_schedules.server_id` + [backend/src/routes/backups.js](backend/src/routes/backups.js) | pas de groupe | `group_id` obligatoire |
| Chat | `chat_sessions.user_id`, `server_id` + [backend/src/routes/chat.js](backend/src/routes/chat.js) | pas de groupe, tools serveur global | `chat_sessions.group_id` + outils groupe-aware |
| Alerts | `alert_rules.server_id`, `user_id` + [backend/src/routes/alerts.js](backend/src/routes/alerts.js) | serveur-global sauf `container_down` | `group_id` + semantics claires selon type d'alerte |
| Uptime | `uptime_monitors.user_id` + [backend/src/routes/uptime.js](backend/src/routes/uptime.js) | pas de groupe | `group_id` obligatoire |
| Webhooks | `webhooks.user_id` + [backend/src/routes/webhooks.js](backend/src/routes/webhooks.js) | pas de groupe | `group_id` obligatoire pour les webhooks de groupe |
| SSL | `ssl_certificates.user_id` + [backend/src/routes/ssl.js](backend/src/routes/ssl.js) | pas de groupe | `group_id` obligatoire |
| Status pages | `status_pages.user_id` + [backend/src/routes/statusPage.js](backend/src/routes/statusPage.js) | pas de groupe | `group_id` obligatoire |
| Annotations | `annotations.server_id`, `user_id` + [backend/src/routes/annotations.js](backend/src/routes/annotations.js) | pas de groupe | `group_id` obligatoire |
| Audit | `audit_logs.user_id` + [backend/src/middleware/audit.js](backend/src/middleware/audit.js) | pas de contexte groupe | ajout `group_id`, `owner_user_id`, `resource_type`, `resource_id` |
| Digest | [backend/src/routes/digest.js](backend/src/routes/digest.js) | pas de groupe, execution asynchrone | owner-only, scope compte global |
| Report | [backend/src/routes/report.js](backend/src/routes/report.js) | pas de groupe, execution asynchrone | owner-only, scope compte global |
| API v1 | [backend/src/routes/api/v1](backend/src/routes/api/v1), [backend/src/middleware/apiAuth.js](backend/src/middleware/apiAuth.js) | pas de groupe, clients externes deja relies au contrat | global owner par defaut, groupe via `X-Group-Id` optionnel |

Note explicite sur `metrics_history` :

- aucune colonne `group_id` n'est ajoutee sur `metrics_history` en V1
- la table reste scopee par `server_id`
- le filtrage groupe se fait indirectement via la liste de serveurs autorises dans le groupe actif

---

## Architecture backend cible

### 1. Nouveau contexte d'acces

Il faut introduire un resolvers central de contexte de groupe.

Nouveaux composants backend recommandes :

- `backend/src/helpers/groupAccess.js`
- `backend/src/middleware/requireGroupContext.js`
- `backend/src/middleware/requireGroupPermission.js`
- `backend/src/helpers/groupScopedServer.js`

### 2. Contrat du contexte de requete

Apres resolution du groupe actif, la requete doit porter un objet stable du type :

```js
req.access = {
  actorUserId: 'uuid-du-membre-ou-owner',
  ownerUserId: 'uuid-du-proprietaire-du-groupe',
  groupId: 'uuid-du-groupe',
  membershipId: 'uuid-du-lien-membre',
  isOwner: true | false,
  permissions: {
    docker: 'write',
    logs: 'read',
    files: 'write',
    terminal: 'none'
  }
};
```

### 3. Selection du groupe actif

Pour rester compatible avec les routes actuelles, deux options existent techniquement :

- routes sous `/groups/:groupId/...`
- ou routes actuelles conservees avec header `X-Group-Id`

Le meilleur compromis avec le code actuel est :

- nouvelles routes de management des groupes sous `/groups`
- routes fonctionnelles existantes conservees
- toutes les routes fonctionnelles proteges lisent `X-Group-Id`

Comportement strict a definir dans `requireGroupContext()` :

- si `X-Group-Id` est present : validation obligatoire du groupe et des permissions
- si `X-Group-Id` est absent et utilisateur owner : mode global owner autorise
- si `X-Group-Id` est absent et utilisateur membre : refus explicite (403) pour eviter tout contournement silencieux

Synthese :

- owner a deux modes : global et groupe
- membre n'a qu'un mode groupe, jamais global

Cas particulier de l'API publique :

- les routes API key sous `/api/v1` doivent conserver la compatibilite backward
- par defaut, un appel API key sans `X-Group-Id` retourne la vue globale owner
- si `X-Group-Id` est fourni, l'appel est scope au groupe cible
- les API keys restent owner-only en V1; un membre ne cree pas de user API key ni d'API key publique de groupe

Point d'entree explicite de la liaison post-inscription :

- dans [backend/src/routes/auth.js](backend/src/routes/auth.js), juste apres creation du compte dans `POST /auth/register`, appeler un service applicatif du type `acceptPendingInvitationsForEmail(email, userId)`
- ajouter un endpoint explicite d'acceptation manuelle : `POST /groups/invitations/accept/:token`
- les deux chemins (auto post-inscription et acceptation manuelle) doivent converger vers la meme fonction de validation/liaison

Fichiers backend cibles a modifier/creer pour ce flux :

- modifier [backend/src/routes/auth.js](backend/src/routes/auth.js) pour brancher `acceptPendingInvitationsForEmail(...)` dans `POST /auth/register`
- creer [backend/src/routes/groups.js](backend/src/routes/groups.js) pour exposer `POST /groups/invitations/accept/:token`
- creer [backend/src/services/groupInvitations.js](backend/src/services/groupInvitations.js) pour centraliser la validation du token, la liaison utilisateur et la creation membre

Contrainte email explicite :

- la liaison automatique ne s'applique que si l'email du compte cree est strictement egal a l'email invite
- si l'email d'inscription ne correspond a aucune invitation valide, aucun rattachement automatique n'est effectue

Raison verifiable : le frontend actuel appelle deja massivement `/servers`, `/servers/:serverId/docker`, `/uptime`, `/webhooks`, `/chat`, etc. depuis [frontend/src/contexts/ServerContext.jsx](frontend/src/contexts/ServerContext.jsx) et les pages de [frontend/src/App.jsx](frontend/src/App.jsx).

### 4. Middleware a introduire

#### `requireGroupContext()`

Responsabilites :

- lire `X-Group-Id`
- verifier que le groupe existe
- verifier que `req.user.id` est owner ou membre actif de ce groupe
- charger les permissions du membre
- charger `owner_user_id`
- remplir `req.access`

#### `requireGroupPermission(zone, minLevel)`

Regles :

- `write` satisfait `read`
- `none` ne satisfait rien
- owner satisfait tout

Pseudo-code :

```js
requireGroupPermission('docker', 'read')
requireGroupPermission('docker', 'write')
requireGroupPermission('logs', 'read')
```

#### `requireGroupServer(serverId)`

Responsabilites :

- verifier que le `server_id` de la route est bien lie au groupe actif via `access_group_servers`
- refuser sinon

#### `requireDockerTargetAccess()`

Responsabilites :

- pour une route Docker sur container/service, verifier que le target demande appartient bien a `access_group_docker_targets`
- refuser sinon

#### `requireFilePathAccess(mode)`

Responsabilites :

- verifier que le path demande est strictement contenu dans une racine de `access_group_file_roots`
- refuser sinon

Regle de securite obligatoire :

- ne jamais utiliser une simple comparaison `startsWith()` sur le chemin brut
- normaliser les chemins avec `path.resolve()` cote Node.js
- comparer ensuite le chemin absolu resolu au prefixe absolu de chaque racine autorisee
- apres resolution, un path est autorise si et seulement si il est egal a la racine ou strictement descendant de cette racine

Exemple de logique correcte :

```js
const resolvedRoot = path.resolve(rootPath);
const resolvedTarget = path.resolve(requestedPath);
const allowed = resolvedTarget === resolvedRoot || resolvedTarget.startsWith(resolvedRoot + path.sep);
```

#### `requireManagedCronAccess()`

Responsabilites :

- ne lister que les entrees de `access_group_cron_entries`
- synchroniser la crontab distante a partir des marqueurs IliaCloud

Strategie de reconciliation obligatoire :

1. lire la crontab distante
2. parser les marqueurs IliaCloud
3. comparer avec `access_group_cron_entries`
4. si une ligne marquee est absente cote serveur mais toujours active en base : la recreer
5. si une ligne marquee existe cote serveur mais plus en base : la supprimer
6. ecrire la crontab reconciliee

Ce comportement couvre explicitement le cas ou un admin supprime manuellement une ligne marquee via `crontab -e`.

### 5. Performance du pipeline de middlewares

Chaque requete protegee traversera potentiellement la chaine : auth JWT -> resolution groupe -> verification membership -> chargement permissions -> verification serveur lie au groupe -> verification target Docker. Cela peut representer 4-5 queries SQL par requete.

Strategie de cache retenue :

- cache en memoire (Map ou LRU) sur le couple `(userId, groupId)` avec un TTL de 60 secondes
- le cache stocke : `{ membership, permissions, ownerUserId, serverIds }`
- invalidation explicite sur les routes mutantes de gestion des groupes/membres (`POST/PATCH/DELETE /groups/...`)
- pas de Redis en V1 (un seul backend, pas de scaling horizontal immediat)
- si le backend passe en multi-instance, migrer vers un cache Redis partage

Consequence : un membre qui vient d'etre revoque peut encore acceder au groupe pendant 60 secondes maximum. Ce delai est acceptable en V1. Pour une revocation instantanee, la route `DELETE /groups/:groupId/members/:memberId` doit aussi invalider le cache.

### 6. Evolution des helpers existants

#### `serverOwnership.js`

Le helper actuel [backend/src/helpers/serverOwnership.js](backend/src/helpers/serverOwnership.js) n'est plus suffisant.

Il doit etre remplace par une resolution group-aware, par exemple :

```js
async function getGroupServerAccess({ actorUserId, groupId, serverId })
```

Ce helper doit verifier en une seule resolution :

- le groupe actif
- le membre actif
- le lien `group -> server`
- le niveau d'acces a la zone demandee

#### `routeHelpers.js`

Le helper actuel [backend/src/helpers/routeHelpers.js](backend/src/helpers/routeHelpers.js) genere deux suppressions types :

- `DELETE ... WHERE id = $1 AND user_id = $2`
- `DELETE ... WHERE id = $1 AND server_id = $2`

Il faut introduire de nouvelles variantes groupe-aware :

- `makeGroupDeleteHandler(table, errorMsg, successBody)`
- `makeGroupServerDeleteHandler(table, errorMsg, successMsg)`

### 7. Impact route par route

#### `/servers`

Fichiers : [backend/src/routes/servers.js](backend/src/routes/servers.js)

Comportement cible :

- `GET /servers` retourne seulement les serveurs lies au groupe actif
- `GET /servers/overview` retourne seulement les serveurs lies au groupe actif
- `GET /servers/:id` verifie aussi `access_group_servers`
- `POST /servers` reste owner-only, car creer un serveur est une operation de compte
- `PATCH /servers/:id` et `DELETE /servers/:id` restent owner-only

#### `/servers/:serverId/docker`

Fichier : [backend/src/routes/docker.js](backend/src/routes/docker.js)

Comportement cible :

- `GET /` liste uniquement les targets resolus par `access_group_docker_targets`
- `GET /stats` filtre les containers de la meme maniere
- `GET /health` filtre les services/containers de la meme maniere
- `GET /stacks` ne remonte que les stacks/compose projects du groupe
- `POST /:containerId/action` exige `docker = write`

#### `/servers/:serverId/logs`

Fichier : [backend/src/routes/logs.js](backend/src/routes/logs.js)

Comportement cible :

- `GET /` retourne uniquement `log_paths` du groupe actif
- `POST /` cree un `log_path` avec `group_id`
- `GET /:id/tail` verifie `group_id`
- `POST /scan` ne propose que des logs rattaches au groupe ou a ses targets Docker

#### `/servers/:serverId/files`

Fichier : [backend/src/routes/files.js](backend/src/routes/files.js)

Comportement cible :

- toutes les routes exigent une racine autorisee dans `access_group_file_roots`
- `browse` interdit de sortir d'une racine du groupe
- `read` exige `files >= read`
- `write` exige `files = write`

#### `/servers/:serverId/quick-actions`

Fichier : [backend/src/routes/quickActions.js](backend/src/routes/quickActions.js)

Comportement cible :

- filtrage par `group_id`
- `GET` exige `quick_actions >= read`
- `POST`, `PATCH`, `DELETE`, `PUT /reorder` exigent `quick_actions = write`
- `POST /:id/run` exige `quick_actions = write`

#### `/servers/:serverId/cron`

Fichier : [backend/src/routes/cron.js](backend/src/routes/cron.js)

Comportement cible :

- abandon du modele par index brut pour la vue groupe-aware
- `GET /` retourne les entrees IliaCloud taguees du groupe
- `POST` cree une ligne `access_group_cron_entries` puis sync la crontab
- `PATCH`, `DELETE`, `POST /toggle` travaillent sur `id`, pas sur index brut

#### `/servers/:serverId/backups`

Fichier : [backend/src/routes/backups.js](backend/src/routes/backups.js)

Comportement cible :

- `backups` et `backup_schedules` portent `group_id`
- toutes les lectures/operations filtrent sur `group_id`
- `read` pour listing/download
- `write` pour creer, supprimer, restaurer, planifier

#### `/chat`

Fichier : [backend/src/routes/chat.js](backend/src/routes/chat.js)

Comportement cible :

- `chat_sessions` doit stocker `group_id`
- `GET /sessions` retourne les sessions du groupe actif
- `POST /sessions` exige `chat >= write`
- `GET /sessions/:id/messages` exige `chat >= read`
- `POST /sessions/:id/message` exige `chat = write`
- tous les tools IA doivent recevoir `groupId` et appliquer les permissions du groupe, pas seulement `serverId`

#### `/alerts`

Fichier : [backend/src/routes/alerts.js](backend/src/routes/alerts.js)

Comportement cible :

- `alert_rules` et `alert_history` portent `group_id`
- `GET /rules/:serverId` filtre par groupe actif
- `POST /rules/:serverId` exige `alerts = write`
- `GET /history` retourne l'historique du groupe actif

Point delicat : les metriques `cpu`, `mem`, `disk`, `load` restent des signaux serveur-globaux.

Notifications d'alerte en contexte groupe :

- quand une alerte se declenche pour un `alert_rule` portant un `group_id`, les destinataires sont :
  - l'owner du groupe (toujours)
  - les membres du groupe ayant `alerts >= read` (notification passive par email/webhook)
- un membre avec `alerts = none` ne recoit aucune notification pour ce groupe
- les alertes sur metriques serveur-globales (`cpu`, `mem`, `disk`, `load`) qui ne portent pas de `group_id` restent owner-only
- les webhooks de notification du groupe (`webhooks` avec `group_id`) sont declenches pour les evenements de ce groupe uniquement
- en V1, les canaux de notification restent ceux de l'owner (pas de preferences de notification par membre)

#### `/uptime`

Fichier : [backend/src/routes/uptime.js](backend/src/routes/uptime.js)

Comportement cible :

- tous les monitors portent `group_id`
- `GET /monitors` retourne les monitors du groupe actif
- `POST`, `PATCH`, `DELETE` exigent `uptime = write`
- `GET /history` exige `uptime >= read`
- `POST /scan/:serverId` ne doit retourner que les URLs liees aux ressources du groupe si le scan est conserve

#### `/digest` et `/report`

Fichiers : [backend/src/routes/digest.js](backend/src/routes/digest.js), [backend/src/routes/report.js](backend/src/routes/report.js)

Comportement cible :

- `digest` et `report` restent owner-only en V1
- ils restent scopes au compte owner global, pas au groupe actif
- aucun digest ni rapport n'est envoye aux membres en V1
- les taches asynchrones ne doivent pas dependre d'un `X-Group-Id`, car ce contexte n'existe pas hors requete interactive

Raison : un digest ou rapport programme s'execute sans "groupe actif" selectionne par l'utilisateur. Le comportement le plus stable et le plus compatible est donc owner-global.

#### `/api/v1`

Fichiers : [backend/src/routes/api/v1](backend/src/routes/api/v1), [backend/src/middleware/apiAuth.js](backend/src/middleware/apiAuth.js)

Comportement cible :

- l'API publique continue a authentifier via API key owner-only
- sans `X-Group-Id`, elle retourne la vue globale du compte owner pour preserver la compatibilite des clients existants
- avec `X-Group-Id`, elle scope la lecture ou l'ecriture au groupe cible
- Swagger doit documenter `X-Group-Id` comme header optionnel pour l'API v1
- toute operation `POST/PATCH/DELETE` sur `/api/v1` avec `X-Group-Id` doit verifier les permissions groupe de la zone concernee

Cas typiques a documenter dans Swagger :

- `GET /api/v1/servers` sans header = liste globale owner
- `GET /api/v1/servers` avec header = liste groupee
- `GET /api/v1/alerts` sans header = historique global owner
- `GET /api/v1/alerts` avec header = historique du groupe

#### `/webhooks`

Fichier : [backend/src/routes/webhooks.js](backend/src/routes/webhooks.js)

Comportement cible :

- les webhooks de projet portent `group_id`
- `read` pour listing
- `write` pour creer, modifier, supprimer, tester

#### `/ssl`

Fichier : [backend/src/routes/ssl.js](backend/src/routes/ssl.js)

Comportement cible :

- `ssl_certificates` porte `group_id`
- lecture/ecriture filtrees par groupe actif

#### `/status-pages`

Fichier : [backend/src/routes/statusPage.js](backend/src/routes/statusPage.js)

Comportement cible :

- `status_pages` porte `group_id`
- les pages de statut appartiennent a un groupe
- les monitors relies a ces pages doivent appartenir au meme groupe

Note explicite sur `status_page_monitors` :

- aucune colonne `group_id` n'est ajoutee sur `status_page_monitors` en V1
- la table herite implicitement du groupe via `status_page_id`
- lors d'un `POST /status-pages/:id/monitors`, le backend doit verifier que `status_pages.group_id = uptime_monitors.group_id`
- cette verification doit etre faite applicativement, ou via trigger SQL si tu veux la rendre non contournable

#### `/audit`

Fichiers : [backend/src/middleware/audit.js](backend/src/middleware/audit.js), [backend/src/routes/audit.js](backend/src/routes/audit.js)

Comportement cible :

- toutes les routes mutantes doivent renseigner `group_id` dans l'audit
- l'ecran audit filtre par groupe actif pour les membres
- l'owner peut voir l'audit global ou par groupe

### 8. WebSocket

Fichier critique : [backend/src/websocket.js](backend/src/websocket.js)

Aujourd'hui le WS verifie uniquement :

- authentification
- `serverId`
- ownership du serveur

Contrat cible minimum :

```json
{ "type": "subscribe", "groupId": "...", "serverId": "..." }
```

Autres messages WS doivent aussi porter `groupId` :

- abonnement dashboard temps reel
- ouverture terminal
- logs Docker streaming

Le WS doit verifier :

- appartenance au groupe
- lien groupe/serveur
- permission de zone demandee
- cible Docker autorisee si applicable

---

## Architecture frontend cible

### 1. Nouveau `GroupContext`

Il faut introduire :

- `frontend/src/contexts/GroupContext.jsx`

Responsabilites :

- charger les groupes accessibles a l'utilisateur
- memoriser le groupe actif dans `localStorage`
- exposer les permissions du membre sur le groupe actif
- injecter `X-Group-Id` sur les appels API via le client central

Injection du header `X-Group-Id` :

Le frontend utilise `fetch` natif via le wrapper [frontend/src/lib/api.js](frontend/src/lib/api.js) (pas Axios ni aucune librairie HTTP tierce). L'injection du header se fait au meme niveau que le pattern existant `setAuthErrorHandler()` :

```js
// dans api.js
let activeGroupId = null;
export function setActiveGroupId(id) { activeGroupId = id; }

// dans api() :
if (activeGroupId) headers['X-Group-Id'] = activeGroupId;
```

Le `GroupContext` appelle `setActiveGroupId(groupId)` a chaque changement de groupe actif. Cela garantit que 100 % des appels API passent par ce point central et portent le header, sans risque d'oubli sur un appel individuel.

Aucun appel `fetch` direct ne doit etre fait en dehors de `api()` pour les routes protegees.

### 2. Evolution de `ServerContext`

Fichier actuel : [frontend/src/contexts/ServerContext.jsx](frontend/src/contexts/ServerContext.jsx)

Changement cible :

- `ServerContext` ne charge plus les serveurs de tout le compte
- il charge les serveurs du groupe actif uniquement
- `activeServerId` devient un choix a l'interieur du groupe actif

Flux cible :

1. l'utilisateur choisit un groupe
2. le frontend recharge les ressources scopees a ce groupe
3. a l'interieur du groupe, il choisit si besoin un serveur actif parmi ceux du groupe

### 3. Navigation et rendu

Le comportement UI attendu est :

- selecteur de groupe global visible dans le layout
- quand un groupe est actif, toutes les pages montrent uniquement ses ressources
- les boutons d'action sont caches ou desactives selon `read` ou `write`
- les pages owner-only restent inaccessibles aux membres

Pages directement impactees :

- [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx)
- [frontend/src/pages/DockerPage.jsx](frontend/src/pages/DockerPage.jsx)
- [frontend/src/pages/LogsPage.jsx](frontend/src/pages/LogsPage.jsx)
- [frontend/src/pages/ActionsPage.jsx](frontend/src/pages/ActionsPage.jsx)
- [frontend/src/pages/ChatPage.jsx](frontend/src/pages/ChatPage.jsx)
- [frontend/src/pages/BackupPage.jsx](frontend/src/pages/BackupPage.jsx)
- [frontend/src/pages/FilesPage.jsx](frontend/src/pages/FilesPage.jsx)
- [frontend/src/pages/CronPage.jsx](frontend/src/pages/CronPage.jsx)
- [frontend/src/pages/UptimePage.jsx](frontend/src/pages/UptimePage.jsx)
- [frontend/src/pages/WebhooksPage.jsx](frontend/src/pages/WebhooksPage.jsx)
- [frontend/src/pages/SSLPage.jsx](frontend/src/pages/SSLPage.jsx)
- [frontend/src/pages/StatusPagesPage.jsx](frontend/src/pages/StatusPagesPage.jsx)
- [frontend/src/pages/AuditPage.jsx](frontend/src/pages/AuditPage.jsx)

### 4. Gestion des groupes et membres

Le plus naturel est d'ajouter un nouvel onglet dans [frontend/src/pages/SettingsPage.jsx](frontend/src/pages/SettingsPage.jsx) :

- `Groupes`

Ce tab doit couvrir :

- creation d'un groupe
- edition d'un groupe
- suppression d'un groupe
- rattachement de serveurs au groupe
- gestion des membres
- envoi d'invitations
- edition de la matrice de permissions par membre
- rattachement des ressources Docker, logs, fichiers, cron, backups, etc.

### 5. Pages owner-only

Doivent rester reservees au proprietaire du compte :

- billing dans [frontend/src/pages/SettingsPage.jsx](frontend/src/pages/SettingsPage.jsx)
- cles SSH
- cles IA
- compte
- gestion des API keys utilisateur
- suppression du compte

### 6. Permissions et UX

Regles UI strictes :

- `none` : la page ou l'action n'apparait pas
- `read` : la page apparait, les formulaires d'ecriture et actions mutantes sont caches ou desactives
- `write` : affichage complet

Exemples :

- `docker = read` : voir la liste et le detail, pas `start/stop/restart`
- `files = read` : browse/read, pas `write`
- `logs = read` : listing et tail, pas ajout/suppression de log paths si on decide que l'edition des log paths releve de `logs = write`; sinon cette zone doit etre montee en `read/write`
- `chat = read` : voir l'historique, pas envoyer de message

### 7. Presets de permissions (V2)

En V1, les permissions sont configurees zone par zone pour chaque membre (17 zones x 3 niveaux). Pour faciliter l'UX, une evolution V2 prevoit des presets de permissions avec surcharge possible :

- `Lecteur` : toutes les zones a `read`, `terminal = none`
- `Operateur` : `docker/files/quick_actions/cron/backups/chat = write`, le reste a `read`, `terminal = none`
- `Admin` : toutes les zones a `write`

L'owner choisit un preset puis peut ajuster zone par zone. Le preset sert de point de depart, pas de contrainte. Les permissions finales stockees restent le JSON complet dans `access_group_members.permissions`.

---

## Quotas et monetisation

### 1. Regle metier exacte

Les limites de plan deja existantes sont :

- Business : 3 utilisateurs
- Enterprise : illimite

Limite supplementaire retenue pour les groupes par serveur :

- Free : 0 groupe par serveur
- Pro : 0 groupe par serveur
- Business : 10 groupes par serveur
- Enterprise : 20 groupes par serveur

Reference : [backend/src/config/plans.js](backend/src/config/plans.js)

### 2. Ce que le quota doit compter

Modele retenu : quota par groupe (pas quota global compte).

Pour un groupe donne, le quota `users` doit compter :

- les membres actifs de ce groupe
- les invitations en attente non expirees de ce groupe

L'owner ne compte pas dans le quota.

Formule cible :

```text
seats_utilises_du_groupe = active_members_du_groupe + pending_invitations_du_groupe
```

Exemple Business :

- limite `users = 3` par groupe
- chaque groupe peut avoir jusqu'a 3 membres en plus de l'owner

### 3. Nouveau middleware recommande

Nouveau middleware :

- `checkSeatQuota()`
- `checkGroupsPerServerQuota()`

Raison : [backend/src/middleware/planLimits.js](backend/src/middleware/planLimits.js) ne sait aujourd'hui compter que des ressources rattachees simplement a `req.user.id`.

Formalisation config plan cible :

- ajouter `groups_per_server` dans [backend/src/config/plans.js](backend/src/config/plans.js)
- valeurs : `free=0`, `pro=0`, `business=10`, `enterprise=20`
- `POST /groups` doit verifier ce quota pour chaque serveur cible avant creation

### 4. Regles d'application

- creation d'invitation : bloque si quota atteint
- activation d'un membre depuis une invitation : revalide le quota
- en cas de downgrade vers `free`, appliquer la regle de coupure globale definie dans la section "Downgrade de plan et coupure des acces membres"
- changement de groupe lors d'une invitation : quota verifie pour le groupe cible uniquement
- creation de groupe : quota `groups_per_server` verifie pour chaque serveur rattache au groupe

### 5. Downgrade de plan et coupure des acces membres

Comportement retenu :

- rappel email a J-7 avant la fin effective de l'abonnement payant
- a l'echeance exacte, si le compte retombe en `free`, tous les acces membres de tous les groupes sont coupes immediatement
- les ressources, groupes, membres et historiques restent en base, mais les membres ne peuvent plus acceder aux groupes

Implementation cible :

- webhook Stripe sur `customer.subscription.deleted`
- webhook Stripe sur `invoice.payment_failed`
- job planifie quotidien pour detecter les abonnements a J-7 et envoyer le rappel
- transition du plan owner dans `subscriptions`
- mise a jour des membres ou du resolver d'acces pour refuser tout membre si `owner.plan = free`

Recommandation technique :

- ne pas supprimer les `access_group_members`
- appliquer la coupure via la logique d'acces, en lisant le plan actuel de l'owner
- garder les donnees intactes pour un futur reupgrade sans restauration manuelle

---

## Strategie de migration des donnees existantes

Le backend execute deja [backend/src/db/schema.sql](backend/src/db/schema.sql) automatiquement via [backend/src/db/autoMigrate.js](backend/src/db/autoMigrate.js). La migration doit donc etre :

- idempotente
- dans `schema.sql`
- compatible avec une base deja peuplee

### 0. Strategie de deploiement sans casse (2 phases)

Phase A (compatibilite) :

- ajouter les nouvelles tables
- ajouter `group_id` nullable sur les tables existantes
- ne pas activer encore le filtrage strict par `group_id` sur toutes les routes
- backfiller les groupes et `group_id`

Phase B (enforcement) :

- activer le filtrage groupe-aware dans toutes les routes ciblees
- ajouter les contraintes `NOT NULL` sur `group_id` uniquement pour les tables dont la migration est complete

Cette sequence evite une coupure prod ou des 404 massifs sur des lignes legacy a `group_id = NULL`.

### 1. Creation des groupes initiaux

Pour chaque utilisateur existant ayant deja des ressources, creer un groupe initial, par exemple :

- `Groupe principal`

Pseudo-SQL de backfill :

```sql
INSERT INTO access_groups (owner_user_id, name, slug)
SELECT u.id, 'Groupe principal', 'groupe-principal'
FROM users u
WHERE EXISTS (SELECT 1 FROM servers s WHERE s.user_id = u.id)
ON CONFLICT DO NOTHING;
```

### 2. Backfill des ressources tablees

Pour chaque table portant des ressources applicatives, remplir `group_id` vers le groupe principal de l'owner :

- `log_paths`
- `quick_actions`
- `chat_sessions`
- `alert_rules`
- `alert_history`
- `backups`
- `backup_schedules`
- `uptime_monitors`
- `webhooks`
- `ssl_certificates`
- `status_pages`
- `annotations`

Point a ne pas oublier :

- `status_page_monitors` ne recoit pas de `group_id`, mais ses lignes deviennent valides ou invalides indirectement selon le `group_id` de `status_pages` et `uptime_monitors`
- apres backfill, il faut verifier qu'aucune liaison `status_page_monitors` ne reference un monitor d'un autre groupe que sa page

Regle de securite migration :

- tant qu'une table n'est pas totalement backfillee, les routes doivent gerer explicitement le mode legacy
- une fois backfill termine et verifie, passer la colonne en `NOT NULL` pour cette table

### 3. Backfill `access_group_servers`

Tous les serveurs d'un owner doivent etre lies a son groupe principal.

### 4. Ressources non backfillables automatiquement a 100 %

Ces cas ne peuvent pas etre repartis par groupe sans choix explicite du proprietaire :

- Docker targets sur serveur partage
- racines fichiers par projet
- cron brut existant
- terminal

Ils doivent donc etre consideres comme "a configurer par l'owner" apres migration.

### 5. Gestion explicite des lignes `group_id IS NULL`

Comportement requis pendant la transition :

- owner en mode global : peut encore voir les lignes legacy `group_id IS NULL`
- owner en mode groupe : ne voit que les lignes `group_id = groupe_actif`
- membre de groupe : ne voit jamais les lignes `group_id IS NULL`

Cette regle supprime l'ambiguite de comportement au premier deploiement.

---

## Tests a creer et a etendre

Cette section liste les tests necessaires pour garantir l'absence de regression et la bonne marche de la feature.

## Backend - nouveaux fichiers de tests

### 1. Resolution du contexte groupe

Creer :

- `backend/tests/groupContext.test.js`

Cas a couvrir :

- owner accede a son groupe
- membre actif accede a son groupe
- utilisateur hors groupe refuse
- groupe inexistant refuse
- `X-Group-Id` manquant : owner passe en mode global
- `X-Group-Id` manquant : membre refuse
- permissions JSON mal formees refusees ou normalisees

### 2. CRUD groupes

Creer :

- `backend/tests/groups.test.js`

Cas a couvrir :

- creation groupe owner
- slug unique par owner
- edition groupe
- suppression logique groupe (`deleted_at`)
- purge admin explicite uniquement
- owner ne peut pas lire les groupes d'un autre owner

### 3. Invitations et membres

Creer :

- `backend/tests/groupInvitations.test.js`
- `backend/tests/groupMembers.test.js`

Cas a couvrir :

- envoi invitation avec email
- hash du token stocke
- token brut jamais stocke en base
- generation token via entropie forte (`randomBytes(32)` attendu en implementation)
- expiration token
- validite 72h
- acceptation invitation
- acceptation apres inscription auto si compte inexistant au moment de l'invitation
- acceptation directe si compte deja existant
- rattachement par email apres creation de compte
- reacceptation impossible
- revocation membre
- reinvitation apres revocation
- email deja membre refuse
- renvoi bloque tant qu'une invitation non expiree existe
- blocage definitif au 6eme envoi si `send_count >= 5`

### 4. Quota de seats

Creer :

- `backend/tests/groupSeatQuota.test.js`

Cas a couvrir :

- Business accepte jusqu'a 3 membres/invitations cumules dans un groupe
- Business bloque au 4eme dans ce groupe
- Enterprise illimite
- invitation expiree ne compte plus
- revocation libere un slot
- un second groupe applique son quota independamment du premier

### 5. Permissions par zone

Creer :

- `backend/tests/groupPermissions.test.js`

Cas a couvrir :

- `none` bloque lecture
- `read` autorise lecture mais bloque ecriture
- `write` autorise lecture et ecriture
- owner bypass tout
- zone inconnue refusee

### 6. Bindings ressources partagees

Creer :

- `backend/tests/groupDockerTargets.test.js`
- `backend/tests/groupFileRoots.test.js`
- `backend/tests/groupCronEntries.test.js`
- `backend/tests/groupServers.test.js`

Cas a couvrir :

- un serveur peut etre lie a plusieurs groupes
- un groupe peut lier plusieurs serveurs
- un target Docker d'un groupe n'apparait pas dans un autre groupe
- un chemin fichier hors racine autorisee est bloque
- un cron non tague n'apparait pas dans la vue groupe
- une entree cron groupee est correctement syncee sur le serveur

### 7. Audit groupe-aware

Creer :

- `backend/tests/auditGroupScope.test.js`

Cas a couvrir :

- insertion `group_id`
- insertion `owner_user_id`
- actor membre correct
- ressource et type de ressource corrects
- donnees sensibles toujours redactees

### 8. WebSocket groupe-aware

Creer :

- `backend/tests/websocketGroupAccess.test.js`

Cas a couvrir :

- handshake `auth` obligatoire avant tout `subscribe`
- JWT jamais passe en query param
- subscribe dashboard autorise sur groupe+serveur autorises
- subscribe refuse hors groupe
- terminal refuse sans permission
- terminal accepte avec permission
- docker logs refuse si target hors groupe

### 9. API publique groupe-aware

Creer :

- `backend/tests/api-v1-groups.test.js`

Cas a couvrir :

- API key owner sans `X-Group-Id` retourne la vue globale
- API key owner avec `X-Group-Id` retourne la vue filtree
- `X-Group-Id` invalide refuse
- ecriture API v1 refusee si permission groupe insuffisante
- Swagger expose bien le header optionnel `X-Group-Id`

## Backend - fichiers existants a etendre

Tous les fichiers ci-dessous existent deja et doivent etre etendus avec des cas groupe-aware :

- [backend/tests/servers.test.js](backend/tests/servers.test.js)
- [backend/tests/docker.test.js](backend/tests/docker.test.js)
- [backend/tests/dockerLogs.test.js](backend/tests/dockerLogs.test.js)
- [backend/tests/logs.test.js](backend/tests/logs.test.js)
- [backend/tests/files.test.js](backend/tests/files.test.js)
- [backend/tests/quickActions.test.js](backend/tests/quickActions.test.js)
- [backend/tests/cron.test.js](backend/tests/cron.test.js)
- [backend/tests/backups.test.js](backend/tests/backups.test.js)
- [backend/tests/chat.test.js](backend/tests/chat.test.js)
- [backend/tests/metrics.test.js](backend/tests/metrics.test.js)
- [backend/tests/alerts.test.js](backend/tests/alerts.test.js)
- [backend/tests/annotations.test.js](backend/tests/annotations.test.js)
- [backend/tests/webhooks.test.js](backend/tests/webhooks.test.js)
- [backend/tests/statusPage.test.js](backend/tests/statusPage.test.js)
- [backend/tests/audit.test.js](backend/tests/audit.test.js)
- [backend/tests/billing.test.js](backend/tests/billing.test.js)
- [backend/tests/api-v1-servers.test.js](backend/tests/api-v1-servers.test.js)
- [backend/tests/api-v1-alerts.test.js](backend/tests/api-v1-alerts.test.js)
- [backend/tests/api-v1-webhooks.test.js](backend/tests/api-v1-webhooks.test.js)
- [backend/tests/api-v1-monitors.test.js](backend/tests/api-v1-monitors.test.js)
- [backend/tests/api-v1-statusPages.test.js](backend/tests/api-v1-statusPages.test.js)
- [backend/tests/api-v1-ssl.test.js](backend/tests/api-v1-ssl.test.js)
- [backend/tests/api-v1-me.test.js](backend/tests/api-v1-me.test.js)

Cas supplementaires a ajouter dans ces fichiers existants :

- membre avec `read` voit seulement le groupe actif
- membre avec `write` peut agir seulement dans le groupe actif
- membre hors groupe recoit 403 ou 404 selon la route
- ressource d'un autre groupe invisible
- owner garde la vision globale quand necessaire
- API v1 conserve la retrocompatibilite sans `X-Group-Id`

## Contrats de securite obligatoires

### 1. Token d'invitation

Le token d'invitation ne doit jamais etre stocke en clair.

Contrat cible :

1. generation du token brut avec une entropie forte, par exemple `crypto.randomBytes(32)`
2. stockage exclusif de `sha256(token)` dans `token_hash`
3. envoi du token brut seulement dans le lien email
4. a l'acceptation, rehacher le token brut recu et comparer au hash stocke

Consequence :

- une fuite de la base ne doit exposer aucun token reutilisable

### 2. Handshake WebSocket

Le JWT ne doit jamais etre passe en query param.

Contrat WS cible :

1. ouverture de la connexion WebSocket sans token en query param
2. premier message obligatoire du client :

```json
{ "type": "auth", "token": "jwt-ou-cookie-fallback" }
```

3. le serveur valide le JWT ou le cookie
4. seulement apres succes, le client peut envoyer :

```json
{ "type": "subscribe", "groupId": "...", "serverId": "..." }
```

5. tout message `subscribe`, `terminal`, `docker_logs` ou equivalent envoye avant `auth` entraine une fermeture de session

Raison :

- les query params fuient dans les logs reverse proxy, logs applicatifs et historiques navigateur
- le handshake en deux temps est la pratique la plus sure avec WebSocket brut

## Frontend - nouveaux fichiers de tests

Creer :

- `frontend/tests/GroupContext.test.jsx`
- `frontend/tests/GroupSelector.test.jsx`
- `frontend/tests/SettingsPage.groups.test.jsx`
- `frontend/tests/SettingsPage.groupMembers.test.jsx`
- `frontend/tests/GroupPermissionMatrix.test.jsx`

Cas a couvrir :

- chargement des groupes accessibles
- persistence du groupe actif
- changement de groupe recharge les ressources
- affichage de la matrice des permissions
- modification des droits membre
- affichage owner-only vs member

## Frontend - fichiers existants a etendre

Etendre les tests existants suivants :

- [frontend/tests/DashboardPage.test.jsx](frontend/tests/DashboardPage.test.jsx)
- [frontend/tests/DockerPage.test.jsx](frontend/tests/DockerPage.test.jsx)
- [frontend/tests/LogsPage.test.jsx](frontend/tests/LogsPage.test.jsx)
- [frontend/tests/ActionsPage.test.jsx](frontend/tests/ActionsPage.test.jsx)
- [frontend/tests/ChatPage.test.jsx](frontend/tests/ChatPage.test.jsx)
- [frontend/tests/BackupPage.test.jsx](frontend/tests/BackupPage.test.jsx)
- [frontend/tests/FilesPage.test.jsx](frontend/tests/FilesPage.test.jsx)
- [frontend/tests/CronPage.test.jsx](frontend/tests/CronPage.test.jsx)
- [frontend/tests/WebhooksPage.test.jsx](frontend/tests/WebhooksPage.test.jsx)
- [frontend/tests/SSLPage.test.jsx](frontend/tests/SSLPage.test.jsx)
- [frontend/tests/StatusPagesPage.test.jsx](frontend/tests/StatusPagesPage.test.jsx)
- [frontend/tests/AuditPage.test.jsx](frontend/tests/AuditPage.test.jsx)
- [frontend/tests/SettingsPage.test.jsx](frontend/tests/SettingsPage.test.jsx)
- [frontend/tests/SettingsPage.servers.test.jsx](frontend/tests/SettingsPage.servers.test.jsx)
- [frontend/tests/SettingsPage.billing.test.jsx](frontend/tests/SettingsPage.billing.test.jsx)

Cas a ajouter :

- groupe actif filtre les donnees affichees
- boutons caches en mode `read`
- boutons visibles en mode `write`
- pages owner-only invisibles pour un membre
- messages d'acces refuse clairs

## E2E - nouveaux scenarios Playwright

Creer des scenarios E2E pour :

- owner cree deux groupes sur un meme serveur et voit des ressources differentes selon le groupe actif
- membre invite dans CoolCare ne voit pas FailDaily
- membre `docker=read` voit les containers sans boutons d'action
- membre `files=write` peut modifier un fichier autorise
- membre sans `terminal` ne peut pas ouvrir le terminal
- cron groupee n'affiche que les entrees du groupe
- audit du groupe affiche bien les actions du membre

---

## Checklist de verification SQL et code avant implementation

### Verification schema actuel

Requetes utiles a lancer :

```sql
SELECT column_name FROM information_schema.columns WHERE table_name = 'servers';
SELECT column_name FROM information_schema.columns WHERE table_name = 'log_paths';
SELECT column_name FROM information_schema.columns WHERE table_name = 'quick_actions';
SELECT column_name FROM information_schema.columns WHERE table_name = 'chat_sessions';
SELECT column_name FROM information_schema.columns WHERE table_name = 'audit_logs';
```

### Verification des tables a grouper

```sql
SELECT 'log_paths' AS table_name, COUNT(*) FROM log_paths
UNION ALL
SELECT 'quick_actions', COUNT(*) FROM quick_actions
UNION ALL
SELECT 'chat_sessions', COUNT(*) FROM chat_sessions
UNION ALL
SELECT 'backups', COUNT(*) FROM backups
UNION ALL
SELECT 'backup_schedules', COUNT(*) FROM backup_schedules
UNION ALL
SELECT 'uptime_monitors', COUNT(*) FROM uptime_monitors
UNION ALL
SELECT 'webhooks', COUNT(*) FROM webhooks
UNION ALL
SELECT 'ssl_certificates', COUNT(*) FROM ssl_certificates
UNION ALL
SELECT 'status_pages', COUNT(*) FROM status_pages
UNION ALL
SELECT 'annotations', COUNT(*) FROM annotations;
```

### Verification du quota de seats

```sql
SELECT name, limits->>'users' AS users_limit
FROM plans
WHERE name IN ('business', 'enterprise');

SELECT name, limits->>'groups_per_server' AS groups_per_server_limit
FROM plans
WHERE name IN ('free', 'pro', 'business', 'enterprise');
```

### Verification du routage actuel

Fichiers a verifier dans le depot :

- [backend/src/index.js](backend/src/index.js)
- [backend/src/routes/servers.js](backend/src/routes/servers.js)
- [backend/src/routes/docker.js](backend/src/routes/docker.js)
- [backend/src/routes/logs.js](backend/src/routes/logs.js)
- [backend/src/routes/files.js](backend/src/routes/files.js)
- [backend/src/routes/quickActions.js](backend/src/routes/quickActions.js)
- [backend/src/routes/cron.js](backend/src/routes/cron.js)
- [backend/src/routes/backups.js](backend/src/routes/backups.js)
- [backend/src/routes/chat.js](backend/src/routes/chat.js)
- [backend/src/routes/alerts.js](backend/src/routes/alerts.js)
- [backend/src/routes/uptime.js](backend/src/routes/uptime.js)
- [backend/src/routes/webhooks.js](backend/src/routes/webhooks.js)
- [backend/src/routes/statusPage.js](backend/src/routes/statusPage.js)
- [backend/src/websocket.js](backend/src/websocket.js)

---

## Redaction correcte de la feature dans la roadmap

Le bloc actuel de [ROADMAP.md](ROADMAP.md) n'est plus exact pour le modele decide.

Le texte actuel :

```md
- [ ] **Partage d'acces (multi-user)**
  - Inviter un utilisateur sur un serveur avec un role (admin, read-only, operator)
  - Permissions granulaires par section (docker, chat, actions, etc.)
  - Audit log de qui fait quoi
  - Business : 3 utilisateurs inclus, Enterprise : illimite
```

Le texte technique correct devrait devenir :

```md
- [ ] **Partage d'acces (multi-user)**
  - Creer des groupes de projets (CoolCare, FailDaily, RollerLogic, etc.)
  - Rattacher a un groupe des ressources venant de un ou plusieurs serveurs
  - Inviter un utilisateur dans un groupe
  - Definir, pour chaque utilisateur et pour chaque zone du groupe, aucun acces, lecture seule ou acces complet
  - Filtrer toute l'UI par groupe actif
  - Audit log de qui fait quoi, dans quel groupe
  - Free/Pro : groupes desactives
  - Business : max 10 groupes par serveur + 3 membres par groupe (owner non compte)
  - Enterprise : max 20 groupes par serveur + membres illimites par groupe
```

---

## Conclusion technique

Le modele exact a implementer n'est pas :

- partage d'un serveur avec un role fixe

Le modele exact a implementer est :

- groupes de projets
- ressources rattachees aux groupes
- serveurs reutilisables par plusieurs groupes
- permissions par membre et par zone
- niveaux `none`, `read`, `write`
- audit groupe-aware
- quotas de seats groupe-aware

Le point le plus important est celui-ci :

- l'isolation par groupe n'est realisable proprement que si les ressources sont modelisees explicitement comme appartenant a un groupe
- pas si l'on reste sur le duo actuel `user_id + server_id`

Le deuxieme point le plus important est celui-ci :

- certaines fonctions actuelles sont structurellement globales au serveur, en particulier le terminal SSH, les metriques machine et la crontab brute
- elles ne peuvent pas etre pretendument "isolees" sans couche technique supplementaire ou sans assumer explicitement leurs limites

Ce document fixe donc le cadre complet, verifiable et non bancal pour implementer la feature sans rework conceptuel ulterieur.