Appearance
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ère | Statut |
|---|---|---|
| 1 | Le design de la page Connexions est revu (carte « Clé API entreprise » + liste des raccourcis) | ✅ |
| 2 | La clé API est stockée dans une table dédiée (enterprise_api_keys), rattachée à enterprise_id, et non plus sur la connexion | ✅ |
| 3 | La 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) | ✅ |
| 4 | La clé API peut être créée puis mise à jour à n'importe quel moment depuis la page Connexions (dialog dédiée) | ✅ |
| 5 | Pé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 | ✅ |
| 6 | Règle d'unicité : une seule connexion par type et par entreprise (le sélecteur masque les types déjà utilisés) | ✅ |
| 7 | Depuis 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
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é ».
- État « non configuré » : icône
- Carte « Connexions API » : liste des raccourcis nommés, badge du type d'application (
Formulairesaujourd'hui), badge statut, menu d'actions (Consulter,Renommer,Supprimer). Pagination conservée.
- En-tête : titre « Connexions », sélecteur d'entreprise (super-admin uniquement, composant
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 depuisenterprise_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é.
- Champ Application (
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 :
encryptcôté serveur, persistance dansenterprise_api_keys(upsert surenterprise_id), statutACTIVE,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 ».
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=…).
- Affiché uniquement si
Connexion = raccourci nommé
- Plus de
baseUrlni deapiKeyPrefixsur la connexion : ces colonnes sont supprimées (tool_16_add_enterprise_api_keys.sql). - Une connexion porte désormais :
name,type(enumApiConnectionType),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).
- Plus de
Erreurs explicites
- Création connexion :
400si le type est déjà utilisé pour cette entreprise, message « Application déjà utilisée pour cette entreprise ». - Création / mise à jour clé :
400si 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 commenotConfigured = true).
- Création connexion :
3. Périmètre fonctionnel
Base de données
| Fichier | Rôle |
|---|---|
sql/tool_13_add_api_connection_type.sql | Création de l'enum ApiConnectionType (FORMULAIRES) + colonne type sur api_connections |
sql/tool_16_add_enterprise_api_keys.sql | Cré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.prisma | Modèle enterprise_api_keys, enum ApiConnectionType, mise à jour de api_connections (suppression des colonnes clé / URL, ajout type) |
Backend
| Fichier | Rôle |
|---|---|
back/src/api/enterprise-api-keys/enterprise-api-keys.module.ts | Module NestJS |
back/src/api/enterprise-api-keys/enterprise-api-keys.controller.ts | GET /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.ts | Upsert 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.ts | Mapping public (préfixe + scopes, jamais la clé en clair) |
back/src/api/enterprise-api-keys/dto/enterprise-api-key.dto.ts | UpsertEnterpriseApiKeyDto, EnterpriseApiKeyResponseDto, EnterpriseListItemDto |
back/src/api/connections/api-type.registry.ts | resolveBaseUrl(type) (lit FORMULAIRES_API_URL), getRequiredScope(type) |
back/src/api/connections/connections.controller.ts | POST/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.ts | Cré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.ts | CreateConnectionDto : name + type. UpdateConnectionDto : name? uniquement |
Frontend
| Fichier | Rôle |
|---|---|
front/src/pages/connections/ConnectionsPage.vue | Refonte 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.vue | Formulaire nom + application (sélecteur de type), bandeaux « clé manquante » et « aucun type disponible » |
front/src/pages/connections/ConnectionBrowsePage.vue | Affichage de l'application (libellé getApiConnectionTypeLabel) à la place de la baseUrl, conservation des parcours forms / cards (BDD-125) |
front/src/components/admin/EnterpriseScopeSelector.vue | Composant de sélection d'entreprise (super-admin) |
front/src/constants/api-connection-types.js | Catalogue des types (FORMULAIRES), getApiConnectionTypeLabel, getAvailableTypeOptions, translateScopes |
front/src/stores/admin-scope-store.js | Pinia : selectedEnterpriseUuid, persistance sessionStorage |
front/src/stores/enterprise-api-key-store.js | Pinia : fetchEnterpriseApiKey, upsertEnterpriseApiKey, gestion du notConfigured (404) |
front/src/stores/enterprises-store.js | Pinia : fetchEnterprises (cache + force, 403 → liste vide), getter enterpriseByUuid |
front/src/stores/connections-store.js | Suppression des champs apiKey / baseUrl, propagation du enterpriseUuid aux endpoints (fetchConnections, createConnection) |
4. Endpoints API (Outil BDD)
| Méthode | Chemin | Guards | Description |
|---|---|---|---|
GET | /enterprise-api-keys | AuthGuard, AdminGuard | Lit la clé API de l'entreprise (préfixe + scopes, jamais la clé) |
PUT | /enterprise-api-keys | AuthGuard, AdminGuard | Crée ou remplace la clé API (check-access console admin, chiffrement, scopes stockés) |
DELETE | /enterprise-api-keys | AuthGuard, AdminGuard | Soft delete de la clé entreprise |
GET | /enterprise-api-keys/enterprises | AuthGuard, SuperAdminGuard | Liste les entreprises (uuid + name) pour le sélecteur super-admin |
POST | /connections | AuthGuard, AdminGuard | Crée un raccourci { name, type } (URL résolue côté serveur, clé lue depuis enterprise_api_keys) |
GET | /connections | AuthGuard, AdminGuard | Liste paginée, filtrable par ?enterpriseUuid= (super-admin) |
PATCH | /connections/:uuid | AuthGuard, AdminGuard | Renomme une connexion |
DELETE | /connections/:uuid | AuthGuard, AdminGuard | Suppression 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
| Code | Cas |
|---|---|
400 | POST /connections : type déjà utilisé pour l'entreprise (« Application déjà utilisée pour cette entreprise ») |
400 | PUT /enterprise-api-keys : clé refusée par la console admin (check-access) |
401 | Jeton absent / invalide |
403 | Rôle non admin (ou SuperAdminGuard violé sur /enterprise-api-keys/enterprises) |
404 | GET /enterprise-api-keys : aucune clé configurée pour cette entreprise (interprété côté front comme notConfigured) |
404 | Connexion 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 +@uniquePrisma).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_URLpour l'instant). Aucun champ URL n'est exposé côté UI. - Scope entreprise :
- Admin :
enterpriseUuidignoré côté serveur (forcé au sien) ; lectures et écritures limitées à son entreprise. - Super-admin :
enterpriseUuidhonoré ; sans paramètre = sa propre entreprise.
- Admin :
- Statut :
ACTIVEaprès check-access OK.ERRORpositionné si un appel distant ultérieur retourne401(markConnectionAsError/ parcours/browse). - Suppression de clé : soft delete (
deleted_at). Les connexions ne sont pas supprimées en cascade ; elles passent enERRORau prochain appel distant. - Mise à jour optimiste front : le store met à jour la liste locale après
PATCH; le refresh defetchConnectionsreste appelé aprèsPUT /enterprise-api-keyspour 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).
- La carte Clé API entreprise est visible : état
- Sur
/connections(super-admin) :- Le sélecteur
EnterpriseScopeSelectorest 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).
- Le sélecteur
- 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.
- Le sous-titre affiche Application puis le nom de la connexion (et plus la
- Vérification API :
PUT /enterprise-api-keysavec mauvaise clé → 400, aucun enregistrement.PUT /enterprise-api-keysavec bonne clé → 200, statutACTIVE, scopes stockés.POST /connections{ type: 'FORMULAIRES' }en double → 400.GET /connectionssuper-admin sans paramètre → connexions de sa propre entreprise ; avec?enterpriseUuid=→ connexions de l'entreprise ciblée.GET /enterprise-api-keyssans clé en base → 404.
8. Tests associés
Backend (unitaires)
back/src/api/enterprise-api-keys/enterprise-api-keys.service.spec.tsgetEnterpriseApiKey: 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.tscreateConnection: refus si type déjà utilisé pour l'entreprise.createConnection: plus d'encryptni defetch(validation déléguée à la clé entreprise).getConnections: filtreenterpriseUuidcôté super-admin.
Backend (e2e)
back/test/e2e/enterprise-api-keys-controller.e2e-spec.tsGET /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.tsPOST /connections: 400 si type déjà utilisé, 201 sans secret.GET /connections: filtreenterpriseUuidsuper-admin.
Frontend (unitaires)
front/src/__tests__/stores/admin-scope-store.spec.js— persistancesessionStorage,setEnterpriseUuid,clear.front/src/__tests__/stores/enterprise-api-key-store.spec.js—fetchEnterpriseApiKey(200, 404, autres erreurs),upsertEnterpriseApiKey, flagsloading/saving/notConfigured.front/src/__tests__/stores/enterprises-store.spec.js—fetchEnterprises(cache, force, 403, autres erreurs), getterenterpriseByUuid.front/src/__tests__/stores/connections-store.spec.js— propagationenterpriseUuid, payload{ name, type }(plus deapiKey/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, propagationenterpriseUuidàcreateConnection.front/src/__tests__/pages/connections/ConnectionBrowsePage.spec.js— sous-titre avec libellé d'application + fallback type brut, parcoursresource=cardau 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).
- Liste vide / connexion ACTIVE (badge type
9. Notes d'implémentation
- Schéma de stockage : la table
enterprise_api_keysest volontairement séparée d'api_connections(et non pas une colonne surenterprises) 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.sqlcopie la clé chiffrée de la connexion la plus récente par entreprise dansenterprise_api_keys, puis supprime les colonnesbase_url,api_key_encrypted,api_key_iv,api_key_prefixdeapi_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'enumApiConnectionType+ une nouvelle variable d'environnement + une entrée dansapi-type.registry.ts. - Scope super-admin : le composant
EnterpriseScopeSelectorréutiliseuseAdminScopeStore; la persistancesessionStoragepermet de retrouver le scope après navigation (notamment entre/connectionset/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-checkcôté applications cibles n'est plus utilisée. - Compatibilité avec BDD-125 : les parcours
form/card(création de source depuisConnectionBrowsePage) 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 champsbaseUrl/apiKeyet 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_byest 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
ApiConnectionTypeintroduit un scope incompatible. - Migration data de sources
API_FORMrattachées à une ancienne connexion : aucune resynchronisation automatique des données déjà extraites (cf. BDD-119 pour la rotation côté sources). - Modification du
typed'une connexion existante : non exposé (équivalent à supprimer puis recréer).
