Skip to content

BDD-126 — Révision de la page Connexions : clé API globale par entreprise

1. User Story

En tant qu'administrateur, je veux définir la clé API Formulaires une seule fois dans un paramétrage global rattaché à mon entreprise, afin de ne pas avoir à la ressaisir à chaque création / modification de connexion ou de source rattachée.

Ce ticket remplace le modèle « clé API par connexion » introduit en [BDD-114] puis prolongé par [BDD-125]. La clé devient une ressource d'entreprise mutualisée entre toutes les applications Hériade.

2. Critères d'acceptation

#CritèreStatut
1Le design de la page Connexions est revu (carte « Clé API entreprise » + liste des raccourcis)
2La clé API est stockée dans une table dédiée (enterprise_api_keys), rattachée à enterprise_id, et non plus sur la connexion
3La création d'une connexion ne demande plus d'URL (résolue via .env) ni de clé — uniquement un nom et un type d'application (sélecteur)
4La clé API peut être créée puis mise à jour à n'importe quel moment depuis la page Connexions (dialog dédiée)
5Périmètre par entreprise : un super-admin peut consulter / définir la clé de n'importe quelle entreprise via un sélecteur ; un admin uniquement la sienne
6Règle d'unicité : une seule connexion par type et par entreprise (le sélecteur masque les types déjà utilisés)
7Depuis le détail d'une connexion (/connections/:uuid/browse), les raccourcis (formulaires / fiches) restent prévisualisables et permettent la création d'une source

Détail fonctionnel des critères

  1. Refonte UI /connections

    • En-tête : titre « Connexions », sélecteur d'entreprise (super-admin uniquement, composant EnterpriseScopeSelector), bouton « Nouvelle connexion » (désactivé si tous les types disponibles sont déjà utilisés).
    • Carte « Clé API entreprise » : préfixe affiché (16 premiers caractères), liste des applications autorisées (chips), date de dernière mise à jour, statut (badge Active / Erreur).
      • État « non configuré » : icône key_off + CTA « Configurer la clé ».
      • État configuré : CTA « Mettre à jour la clé ».
    • Carte « Connexions API » : liste des raccourcis nommés, badge du type d'application (Formulaires aujourd'hui), badge statut, menu d'actions (Consulter, Renommer, Supprimer). Pagination conservée.
  2. Création d'une connexion (/connections/create)

    • Champ Application (q-select) : liste filtrée des types non encore associés à une connexion pour l'entreprise scopée. Description de chaque type + scope requis affichés sous le sélecteur.
    • Champ Nom de la connexion (libellé personnalisé, ≤ 255 caractères).
    • Plus de champ URL ni clé API — l'URL est résolue côté serveur via FORMULAIRES_API_URL (.env), la clé est lue depuis enterprise_api_keys.
    • Si la clé entreprise n'est pas configurée → bandeau bloquant + bouton Enregistrer désactivé.
    • Si tous les types sont déjà utilisés → bandeau d'info + formulaire désactivé.
  3. Carte « Clé API entreprise » — création / rotation

    • Dialog « Clé API globale » avec champ password + toggle œil.
    • Bouton Enregistrer désactivé tant que la clé est vide après trim.
    • À la validation : vérification distante auprès de la console admin (/check-access) → si refusée, la clé n'est pas persistée (toast d'erreur, dialog conservée).
    • Si OK : encrypt côté serveur, persistance dans enterprise_api_keys (upsert sur enterprise_id), statut ACTIVE, last_validated_at = now, scopes disponibles stockés en JSON (available_scopes).
    • Après succès : refresh de la liste des connexions (les statuts dépendant de la clé peuvent évoluer) + toast « Clé API globale mise à jour ».
  4. Sélecteur d'entreprise super-admin (EnterpriseScopeSelector)

    • Affiché uniquement si isSuperAdmin. Pour un admin classique, le scope est implicite (son entreprise, enterpriseUuid = null).
    • Liste fournie par GET /enterprise-api-keys/enterprises (super-admin uniquement).
    • Sélection persistée en sessionStorage (admin-scope:selected-enterprise-uuid) → le scope est partagé entre la page Connexions et le wizard de création.
    • Sélectionner sa propre entreprise revient à effacer le scope (null).
    • À chaque changement : refresh des connexions et de la clé pour la nouvelle entreprise (?enterpriseUuid=…).
  5. Connexion = raccourci nommé

    • Plus de baseUrl ni de apiKeyPrefix sur la connexion : ces colonnes sont supprimées (tool_16_add_enterprise_api_keys.sql).
    • Une connexion porte désormais : name, type (enum ApiConnectionType), enterprise_id, status, last_validated_at, error_message.
    • Règle d'unicité 1 connexion par (enterprise_id, type) : pas de contrainte SQL pour ne pas bloquer un backfill, mais filtrage UI (getAvailableTypeOptions) + contrôle serveur côté création.
    • Renommage et suppression : inchangés vs BDD-116 (PATCH { name }, DELETE hard).
  6. Erreurs explicites

    • Création connexion : 400 si le type est déjà utilisé pour cette entreprise, message « Application déjà utilisée pour cette entreprise ».
    • Création / mise à jour clé : 400 si la clé est refusée par la console admin ; le message remonté côté front (response.data.message) est affiché tel quel.
    • GET clé pour une entreprise sans clé : 404 (le front l'interprète comme notConfigured = true).

3. Périmètre fonctionnel

Base de données

FichierRôle
sql/tool_13_add_api_connection_type.sqlCréation de l'enum ApiConnectionType (FORMULAIRES) + colonne type sur api_connections
sql/tool_16_add_enterprise_api_keys.sqlCréation de la table enterprise_api_keys, backfill depuis api_connections (connexion la plus récente par entreprise), suppression des colonnes base_url, api_key_encrypted, api_key_iv, api_key_prefix sur api_connections
back/prisma/schema.prismaModèle enterprise_api_keys, enum ApiConnectionType, mise à jour de api_connections (suppression des colonnes clé / URL, ajout type)

Backend

FichierRôle
back/src/api/enterprise-api-keys/enterprise-api-keys.module.tsModule NestJS
back/src/api/enterprise-api-keys/enterprise-api-keys.controller.tsGET /enterprise-api-keys, PUT /enterprise-api-keys, DELETE /enterprise-api-keys, GET /enterprise-api-keys/enterprises
back/src/api/enterprise-api-keys/enterprise-api-keys.service.tsUpsert chiffré, check-access console admin, scopes disponibles, résolution du scope entreprise (admin / super-admin)
back/src/api/enterprise-api-keys/enterprise-api-key.presenter.tsMapping public (préfixe + scopes, jamais la clé en clair)
back/src/api/enterprise-api-keys/dto/enterprise-api-key.dto.tsUpsertEnterpriseApiKeyDto, EnterpriseApiKeyResponseDto, EnterpriseListItemDto
back/src/api/connections/api-type.registry.tsresolveBaseUrl(type) (lit FORMULAIRES_API_URL), getRequiredScope(type)
back/src/api/connections/connections.controller.tsPOST/GET/PATCH/DELETE /connections — plus de baseUrl ni apiKey dans les DTO, ajout du ?enterpriseUuid= côté super-admin
back/src/api/connections/connections.service.tsCréation connexion = vérifie unicité (enterprise_id, type), plus de chiffrement de clé (déléguée à enterprise_api_keys)
back/src/api/connections/dto/connection.dto.tsCreateConnectionDto : name + type. UpdateConnectionDto : name? uniquement

Frontend

FichierRôle
front/src/pages/connections/ConnectionsPage.vueRefonte complète : carte clé API entreprise, sélecteur scope super-admin, liste des connexions (avec badge type), dialog clé globale
front/src/pages/connections/ConnectionCreatePage.vueFormulaire nom + application (sélecteur de type), bandeaux « clé manquante » et « aucun type disponible »
front/src/pages/connections/ConnectionBrowsePage.vueAffichage de l'application (libellé getApiConnectionTypeLabel) à la place de la baseUrl, conservation des parcours forms / cards (BDD-125)
front/src/components/admin/EnterpriseScopeSelector.vueComposant de sélection d'entreprise (super-admin)
front/src/constants/api-connection-types.jsCatalogue des types (FORMULAIRES), getApiConnectionTypeLabel, getAvailableTypeOptions, translateScopes
front/src/stores/admin-scope-store.jsPinia : selectedEnterpriseUuid, persistance sessionStorage
front/src/stores/enterprise-api-key-store.jsPinia : fetchEnterpriseApiKey, upsertEnterpriseApiKey, gestion du notConfigured (404)
front/src/stores/enterprises-store.jsPinia : fetchEnterprises (cache + force, 403 → liste vide), getter enterpriseByUuid
front/src/stores/connections-store.jsSuppression des champs apiKey / baseUrl, propagation du enterpriseUuid aux endpoints (fetchConnections, createConnection)

4. Endpoints API (Outil BDD)

MéthodeCheminGuardsDescription
GET/enterprise-api-keysAuthGuard, AdminGuardLit la clé API de l'entreprise (préfixe + scopes, jamais la clé)
PUT/enterprise-api-keysAuthGuard, AdminGuardCrée ou remplace la clé API (check-access console admin, chiffrement, scopes stockés)
DELETE/enterprise-api-keysAuthGuard, AdminGuardSoft delete de la clé entreprise
GET/enterprise-api-keys/enterprisesAuthGuard, SuperAdminGuardListe les entreprises (uuid + name) pour le sélecteur super-admin
POST/connectionsAuthGuard, AdminGuardCrée un raccourci { name, type } (URL résolue côté serveur, clé lue depuis enterprise_api_keys)
GET/connectionsAuthGuard, AdminGuardListe paginée, filtrable par ?enterpriseUuid= (super-admin)
PATCH/connections/:uuidAuthGuard, AdminGuardRenomme une connexion
DELETE/connections/:uuidAuthGuard, AdminGuardSuppression définitive

Corps PUT /enterprise-api-keys

json
{ "apiKey": "heriade_frm_secret_xyz" }

Réponse 200 /enterprise-api-keys

json
{
  "uuid": "5fae2b1f-…",
  "apiKeyPrefix": "heriade_frm_xxxx",
  "status": "ACTIVE",
  "errorMessage": null,
  "availableScopes": ["formulaires:read"],
  "lastValidatedAt": "2026-05-28T12:00:00.000Z",
  "updatedAt": "2026-05-28T12:00:00.000Z"
}

Corps POST /connections

json
{
  "name": "API Formulaires prod",
  "type": "FORMULAIRES"
}

Super-admin : ajouter ?enterpriseUuid=<uuid> en query pour cibler une autre entreprise.

Erreurs courantes

CodeCas
400POST /connections : type déjà utilisé pour l'entreprise (« Application déjà utilisée pour cette entreprise »)
400PUT /enterprise-api-keys : clé refusée par la console admin (check-access)
401Jeton absent / invalide
403Rôle non admin (ou SuperAdminGuard violé sur /enterprise-api-keys/enterprises)
404GET /enterprise-api-keys : aucune clé configurée pour cette entreprise (interprété côté front comme notConfigured)
404Connexion introuvable ou hors périmètre entreprise

5. Règles métier implémentées

  • Une clé par entreprise : contrainte d'unicité enterprise_api_keys.enterprise_id (index unique SQL + @unique Prisma). PUT = upsert.
  • Une connexion par type et par entreprise : non contrainte au niveau SQL pour ne pas bloquer le backfill, contrôlée à la création (back + filtrage front via getAvailableTypeOptions).
  • Chiffrement : la clé est chiffrée (encryptHelper, AES-256-CTR) avant persistance ; jamais renvoyée en clair. Seul le préfixe (16 premiers caractères) est exposé.
  • Check-access : à chaque PUT, appel à la console admin (/check-access) avec la clé candidate pour valider l'accès et récupérer les scopes disponibles ; si refus → pas de persistance.
  • URL résolue côté serveur : resolveBaseUrl(type) lit la variable d'env correspondante (FORMULAIRES_API_URL pour l'instant). Aucun champ URL n'est exposé côté UI.
  • Scope entreprise :
    • Admin : enterpriseUuid ignoré côté serveur (forcé au sien) ; lectures et écritures limitées à son entreprise.
    • Super-admin : enterpriseUuid honoré ; sans paramètre = sa propre entreprise.
  • Statut : ACTIVE après check-access OK. ERROR positionné si un appel distant ultérieur retourne 401 (markConnectionAsError / parcours /browse).
  • Suppression de clé : soft delete (deleted_at). Les connexions ne sont pas supprimées en cascade ; elles passent en ERROR au prochain appel distant.
  • Mise à jour optimiste front : le store met à jour la liste locale après PATCH ; le refresh de fetchConnections reste appelé après PUT /enterprise-api-keys pour propager les changements de statut éventuels.

6. Variables d'environnement

ini
# back/.env — résolution des URLs API par type
FORMULAIRES_API_URL=https://api.formulaires.heriade.test

# Console admin (check-access lors d'un PUT /enterprise-api-keys)
CONSOLE_ADMIN_API_URL=...
CONSOLE_ADMIN_API_KEY=...

# Chiffrement (réutilisé de BDD-114)
ENCRYPTION_KEY=<secret-32-chars-min>

Migrations à appliquer côté base tool :

bash
psql "$DATABASE_URL_TOOL" -f sql/tool_13_add_api_connection_type.sql
psql "$DATABASE_URL_TOOL" -f sql/tool_16_add_enterprise_api_keys.sql
npx prisma generate   # depuis back/

7. Points de vérification

  • Sur /connections (admin) :
    • La carte Clé API entreprise est visible : état key_off + CTA « Configurer la clé » si rien n'est en base, sinon préfixe + chips d'applications autorisées + CTA « Mettre à jour la clé ».
    • Le bouton Nouvelle connexion est désactivé tant que la clé n'est pas configurée ou si tous les types disponibles sont déjà associés à une connexion.
    • Le menu d'actions d'une connexion contient Consulter / Renommer / Supprimer (plus de « Modifier la clé API » par connexion).
  • Sur /connections (super-admin) :
    • Le sélecteur EnterpriseScopeSelector est visible.
    • Changer d'entreprise déclenche un refresh des connexions et de la clé (?enterpriseUuid=... propagé).
    • Le scope est conservé entre la liste et le wizard de création (sessionStorage).
  • Sur /connections/create :
    • Un seul champ texte (nom) + un sélecteur d'application.
    • Aucun champ URL ni clé API.
    • Bandeau bloquant si la clé entreprise est manquante (lien vers /connections).
    • Bandeau d'info si tous les types sont déjà utilisés.
  • Sur /connections/:uuid/browse :
    • Le sous-titre affiche Application puis le nom de la connexion (et plus la baseUrl).
    • Les parcours formulaires et fiches (BDD-125) restent fonctionnels.
  • Vérification API :
    • PUT /enterprise-api-keys avec mauvaise clé → 400, aucun enregistrement.
    • PUT /enterprise-api-keys avec bonne clé → 200, statut ACTIVE, scopes stockés.
    • POST /connections { type: 'FORMULAIRES' } en double → 400.
    • GET /connections super-admin sans paramètre → connexions de sa propre entreprise ; avec ?enterpriseUuid= → connexions de l'entreprise ciblée.
    • GET /enterprise-api-keys sans clé en base → 404.

8. Tests associés

Backend (unitaires)

  • back/src/api/enterprise-api-keys/enterprise-api-keys.service.spec.ts
    • getEnterpriseApiKey : périmètre admin / super-admin, 404 si non configuré.
    • upsertEnterpriseApiKey : check-access OK / KO, chiffrement, persistance des scopes, reset du statut.
    • deleteEnterpriseApiKey : soft delete + 404 si absente.
    • listEnterprises : super-admin uniquement.
  • back/src/api/connections/api-type.registry.spec.ts : resolveBaseUrl (env présent / absent), getRequiredScope.
  • back/src/api/connections/connections.service.spec.ts
    • createConnection : refus si type déjà utilisé pour l'entreprise.
    • createConnection : plus d'encrypt ni de fetch (validation déléguée à la clé entreprise).
    • getConnections : filtre enterpriseUuid côté super-admin.

Backend (e2e)

  • back/test/e2e/enterprise-api-keys-controller.e2e-spec.ts
    • GET /enterprise-api-keys : 401, 403, 404, 200.
    • PUT /enterprise-api-keys : 401, 403, 400 check-access, 200 + reset statut.
    • DELETE /enterprise-api-keys : 401, 403, 404, 200 + soft delete.
    • GET /enterprise-api-keys/enterprises : 401, 403 (admin classique), 200 (super-admin).
  • back/test/e2e/connections-controller.e2e-spec.ts
    • POST /connections : 400 si type déjà utilisé, 201 sans secret.
    • GET /connections : filtre enterpriseUuid super-admin.

Frontend (unitaires)

  • front/src/__tests__/stores/admin-scope-store.spec.js — persistance sessionStorage, setEnterpriseUuid, clear.
  • front/src/__tests__/stores/enterprise-api-key-store.spec.jsfetchEnterpriseApiKey (200, 404, autres erreurs), upsertEnterpriseApiKey, flags loading / saving / notConfigured.
  • front/src/__tests__/stores/enterprises-store.spec.jsfetchEnterprises (cache, force, 403, autres erreurs), getter enterpriseByUuid.
  • front/src/__tests__/stores/connections-store.spec.js — propagation enterpriseUuid, payload { name, type } (plus de apiKey / baseUrl).
  • front/src/__tests__/components/admin/EnterpriseScopeSelector.spec.js — visibilité super-admin uniquement, chargement initial des entreprises, options, sélection / reset.
  • front/src/__tests__/pages/connections/ConnectionsPage.spec.js — carte clé API (états vide / configuré), dialog upsert, scope super-admin, désactivation « Nouvelle connexion » si tous les types utilisés.
  • front/src/__tests__/pages/connections/ConnectionCreatePage.spec.js — sélecteur application (présélection auto), bandeaux clé manquante / aucun type, propagation enterpriseUuid à createConnection.
  • front/src/__tests__/pages/connections/ConnectionBrowsePage.spec.js — sous-titre avec libellé d'application + fallback type brut, parcours resource=card au montage.

Frontend (e2e Playwright)

  • front/e2e/pages/connections/connections.spec.js
    • Liste vide / connexion ACTIVE (badge type Formulaires) / ERROR.
    • Désactivation Nouvelle connexion si tous les types sont déjà utilisés.
    • Happy path création : un seul nom, application présélectionnée, payload { name, type: 'FORMULAIRES' }.
    • Bandeau « clé API non configurée » sur la page de création.
    • Carte clé API entreprise : état vide → configuration via dialog, mise à jour, validation vide, erreur API, annulation.
    • Menu d'actions : Consulter / Renommer / Supprimer (absence explicite de « Modifier la clé API »).
    • Parcours /connections/:uuid/browse (formulaires + fiches, BDD-125).

9. Notes d'implémentation

  • Schéma de stockage : la table enterprise_api_keys est volontairement séparée d'api_connections (et non pas une colonne sur enterprises) pour conserver l'audit (updated_by, last_validated_at, soft delete) et préparer l'éventuelle extension à plusieurs clés par entreprise (par scope, par environnement…).
  • Backfill : la migration tool_16_add_enterprise_api_keys.sql copie la clé chiffrée de la connexion la plus récente par entreprise dans enterprise_api_keys, puis supprime les colonnes base_url, api_key_encrypted, api_key_iv, api_key_prefix de api_connections. La transition est donc transparente pour les entreprises ayant déjà une connexion.
  • URL serveur uniquement : la résolution via .env (resolveBaseUrl) évite toute saisie utilisateur. Une nouvelle application (autre que Formulaires) nécessite l'ajout d'une entrée dans l'enum ApiConnectionType + une nouvelle variable d'environnement + une entrée dans api-type.registry.ts.
  • Scope super-admin : le composant EnterpriseScopeSelector réutilise useAdminScopeStore ; la persistance sessionStorage permet de retrouver le scope après navigation (notamment entre /connections et /connections/create).
  • Validation de la clé : décalée à la console admin (/check-access). C'est la seule API capable de valider une clé + retourner les scopes — la vérification distante via /connection-check côté applications cibles n'est plus utilisée.
  • Compatibilité avec BDD-125 : les parcours form / card (création de source depuis ConnectionBrowsePage) sont conservés. Le ticket BDD-125 reste valide sur sa partie « ressource API XOR » et payload sources.
  • Tests adaptés : tous les stubs / mocks de tests (connections-store, ConnectionsPage, ConnectionCreatePage, ConnectionBrowsePage + e2e) ont été remis en cohérence avec la suppression des champs baseUrl / apiKey et l'apparition du sélecteur de scope.

10. Hors périmètre

  • Rotation automatique de la clé API entreprise (cron, expiration). La rotation reste manuelle via la dialog.
  • Audit log détaillé des modifications de la clé (qui a changé quoi, à quelle date) — seul updated_by est stocké aujourd'hui.
  • Plusieurs clés par entreprise (ex. une clé par application). Le modèle actuel impose une clé unique mutualisée — extension prévue si un second ApiConnectionType introduit un scope incompatible.
  • Migration data de sources API_FORM rattachées à une ancienne connexion : aucune resynchronisation automatique des données déjà extraites (cf. BDD-119 pour la rotation côté sources).
  • Modification du type d'une connexion existante : non exposé (équivalent à supprimer puis recréer).