Skip to content

BDD-125 — Revue des accès API (mise à jour de connexion)

1. User Story

En tant qu'administrateur, je veux pouvoir renommer une connexion API et remplacer sa clé API sans avoir à la supprimer puis recréer, afin de gérer le cycle de vie d'une connexion (rotation de clé, ajustement d'intitulé) tout en conservant les sources déjà rattachées.

Ce ticket complète BDD-117 (création/listing/suppression de connexion) et BDD-118 (sources API_FORM).

2. Critères d'acceptation

#CritèreStatut
1L'administrateur peut renommer une connexion existante sans rompre les sources rattachées
2L'administrateur peut remplacer la clé API d'une connexion existante (chiffrement + reset statut ACTIVE)
3Les deux modifications passent par un seul endpoint PATCH /connections/:uuid (champs combinables ou indépendants)
4En cas d'erreur de validation ou connexion introuvable, aucune modification n'est persistée et un message explicite est affiché
5Le adminGuard du front fonctionne avec la signature Vue Router 4 (retourne true / route) et tolère une session non encore hydratée

Détail fonctionnel des critères

  1. Renommage

    • Action « Renommer » dans le menu de chaque ligne de la liste /connections (édition inline q-input avec ✓ / ✕).
    • Enter valide, Échap annule, bouton ✓ désactivé si le nom est vide ou > 255 caractères.
    • Pas de modification de la clé ni d'appel API distant (seul le libellé change).
  2. Modification de clé API

    • Action « Modifier la clé API » dans le menu (icône vpn_key).
    • Ouvre une dialog dédiée : champ password avec toggle visibilité (visibility / visibility_off), bouton Annuler / Enregistrer.
    • Côté serveur : chiffrement immédiat (encrypt), persistance, statut remis à ACTIVE, error_message effacé, last_validated_at mis à jour. Pas d'appel à /connection-check au PATCH (aligné sur POST /connections).
    • Le api_key_prefix est recalculé (16 premiers caractères) — la liste affiche immédiatement le nouveau préfixe.
    • Validité opérationnelle : détectée lors des appels distants (parcours /connections/:uuid/browse, sources API) ; un 401 distant déclenche markConnectionAsError (statut ERROR).
  3. Endpoint unifié

    • PATCH /connections/:uuid accepte { name?, apiKey? } (au moins l'un des deux est requis).
    • Validation class-validator : champs optionnels, mais chacun, s'il est fourni, doit être une chaîne non vide.
    • 400 BadRequest si le body est vide ou si les deux champs sont blancs après trim.
  4. Erreurs explicites

    • PATCH : 400 si body vide / champs blancs après trim ; 404 si connexion introuvable ou hors périmètre entreprise.
    • Clé réellement invalide : pas rejetée au PATCH ; la connexion passe en ERROR lors du prochain appel API distant (markConnectionAsError, message « Clé API invalide ou révoquée. »).
    • Côté front : notify.error(extractErrorMessage(error, …)) sur échec PATCH ; la dialog clé API reste ouverte (renommage : fermeture immédiate).
  5. Garde adminGuard

    • Migration vers la signature promise-based (Vue Router 4) : retourne true, '/upload', ou une RouteLocation.
    • Si userStore.user est null (rechargement direct sur /connections), appel à getMe() avant décision.
    • Si getMe() échoue (réseau, 401) → redirection vers '/upload' plutôt qu'erreur non gérée.

3. Périmètre fonctionnel

Objectif : permettre la création d'une source depuis ConnectionBrowsePage pour une ressource de type fiche (submissions), en plus du formulaire (responses).

Backend

FichierRôle
back/src/api/connections/dto/connection.dto.tsUpdateConnectionDto : name? optionnel, ajout apiKey? (avec @IsOptional @IsString @IsNotEmpty)
back/src/api/connections/connections.service.tsupdateConnection étendue : construit updateData selon les champs fournis ; si apiKey présent → encrypt() puis mise à jour api_key_encrypted / api_key_iv / api_key_prefix / status=ACTIVE / error_message=null / last_validated_at. markConnectionAsError pour le suivi ERROR à l'usage. BadRequestException si aucun champ exploitable
back/src/api/connections/connections.controller.tsMise à jour du @ApiOperation (« Mettre à jour une connexion API (nom et/ou clé) »)
back/src/api/sources/dto/source.dto.tsExtension du contrat source API : cardUuid?, cardTitle?, apiResourceType?: 'form' | 'card', en conservant formUuid/formTitle
back/src/api/sources/sources.controller.tsGET /sources/api-preview accepte désormais formUuid ou cardUuid (XOR), avec labels optionnels (formTitle, cardTitle)
back/src/api/sources/sources.service.tsUnification du flux de création/preview API : résolution de ressource (form/card) puis appel à loadFormResponses ou loadCardSubmissions

Frontend

FichierRôle
front/src/stores/connections-store.jsupdateConnection({ connectionUuid, name?, apiKey? }) : payload construit conditionnellement (typeof === 'string'), mise à jour optimiste de la liste locale via connections[i] = { ...connections[i], ...data }
front/src/pages/connections/ConnectionsPage.vueNouvelle action « Modifier la clé API » dans le menu (icône vpn_key, séparateur), nouvelle dialog showEditApiKeyDialog avec toggle visibilité et bouton Enregistrer désactivé tant que la clé est vide, helpers startEditApiKey / cancelEditApiKey / confirmEditApiKey
front/src/pages/connections/ConnectionCreatePage.vueWording : « générée dans Console Admin » + sous-titre d'aide ; placeholder clé heriade_… (générique)
front/src/router/adminGuard.jsMigration en async : retourne true ou '/upload', charge getMe() si user absent, fallback '/upload' en cas d'échec
front/src/pages/connections/ConnectionBrowsePage.vueLe bouton Créer une source est disponible pour form et card; redirection vers /sources/create avec formUuid/formTitle ou cardUuid/cardTitle
front/src/pages/admin/sources/AdminSourceCreatePage.vuePré-remplissage API enrichi (cardUuid/cardTitle), preview et submit pilotés par apiResourceType
front/src/stores/source-store.jsgetApiSourcePreview et createSource enrichis pour supporter cardUuid/cardTitle/apiResourceType

Aucune migration de base nécessaire — la table api_connections contient déjà tous les champs (api_key_encrypted, api_key_iv, api_key_prefix, status, error_message, last_validated_at) introduits par BDD-117.

4. Endpoints API

MéthodeCheminGuardsDescription
PATCH/connections/:uuidAuthGuard, AdminGuardMet à jour une connexion : name, apiKey, ou les deux
GET/sources/api-previewAuthGuard, AdminGuardPrévisualise une ressource API (formUuid ou cardUuid)
POST/sourcesAuthGuard, AdminGuardCrée une source API depuis formulaire ou fiche

Corps PATCH /connections/:uuid — renommage seul

json
{ "name": "API Formulaires prod" }

Corps PATCH /connections/:uuid — rotation de clé seule

json
{ "apiKey": "heriade_frm_nouvelle_secret_xyz" }

Corps PATCH /connections/:uuid — mise à jour combinée

json
{
  "name": "API Formulaires staging",
  "apiKey": "heriade_frm_staging_secret"
}

Réponse 200 (extrait)

json
{
  "uuid": "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
  "name": "API Formulaires prod",
  "baseUrl": "https://api.formulaires.test",
  "apiKeyPrefix": "heriade_frm_xxxx",
  "status": "ACTIVE",
  "errorMessage": null,
  "lastValidatedAt": "2026-05-27T10:00:00.000Z",
  "createdAt": "2026-05-18T10:00:00.000Z",
  "updatedAt": "2026-05-27T10:00:00.000Z"
}

Erreurs courantes

CodeCas
400Body vide / name et apiKey tous deux absents ou blancs après trim (PATCH /connections/:uuid)
400GET /sources/api-preview : formUuid et cardUuid absents ou fournis simultanément
401Jeton absent / invalide
403Rôle non admin
404Connexion introuvable ou hors périmètre entreprise (ApiConnectionNotFoundException)

Complément payload POST /sources (mode API)

Cas formulaire :

json
{
  "source": "api",
  "connectionUuid": "uuid-connexion",
  "formUuid": "uuid-formulaire",
  "formTitle": "Inscriptions 2026",
  "apiResourceType": "form",
  "name": "Source inscriptions",
  "tableName": "inscriptions_2026"
}

Cas fiche :

json
{
  "source": "api",
  "connectionUuid": "uuid-connexion",
  "cardUuid": "uuid-fiche",
  "cardTitle": "Fiche Clients",
  "apiResourceType": "card",
  "name": "Source soumissions clients",
  "tableName": "soumissions_clients"
}

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

  • Périmètre entreprise : un admin met à jour uniquement les connexions de son enterprise_id ; un super-admin n'est pas filtré (buildEnterpriseWhere).
  • Trim systématique côté serveur (dto.name?.trim(), dto.apiKey?.trim()) avant validation/persistance.
  • Pas de pré-vérification au PATCH ni au POST /connections : la clé est chiffrée et stockée ; la validité est constatée à l'usage (parcours, preview source, etc.).
  • Reset du statut : une rotation de clé via PATCH repose les compteurs (status = ACTIVE, error_message = null, last_validated_at = now) même si l'ancienne clé était en ERROR — sans garantir que la nouvelle clé soit valide tant qu'aucun appel distant n'a réussi.
  • Marquage erreur : markConnectionAsError (appelé depuis connection-browse.service sur 401 distant) repasse la connexion en ERROR sans modifier la clé stockée.
  • Prefix recalculé systématiquement sur la nouvelle clé (buildApiKeyPrefix, 16 premiers caractères).
  • Mise à jour optimiste côté store : remplacement de l'élément dans connections[] par fusion ({ ...existing, ...data }) — pas de refetch global.
  • Sources rattachées : aucun impact. Une source API_FORM (cf. BDD-118) reste exploitable ; ses données déjà chargées ne sont pas resynchronisées (hors périmètre, prévu en BDD-119).
  • Ressource API (création/preview) : en mode source='api', exactement une ressource doit être fournie (formUuid ou cardUuid).
  • Compatibilité ascendante : le flux historique formUuid/formTitle reste inchangé.
  • Persistance inchangée : pas de migration DB ; les sources issues de fiches restent stockées avec source_type='API_FORM'.

6. Points de vérification

  • Sur /connections → menu d'actions d'une ligne : présence de Consulter, Renommer, Modifier la clé API, Supprimer dans cet ordre, avec séparateurs.
  • Renommage : édition inline, validation Enter, annulation Échap / bouton ✕, toast « Connexion renommée », pas de modification si entrée vide ; un échec backend reset l'édition et affiche le message d'erreur.
  • Rotation de clé : dialog « Mettre à jour la clé API » affiche le nom de la connexion ; Enregistrer désactivé tant que le champ est vide ; toggle œil pour afficher/masquer la clé ; après succès → toast « Clé API mise à jour », dialog fermée, préfixe rafraîchi dans la liste, badge Active (même si la connexion était auparavant en Erreur).
  • Erreur PATCH (validation, 404) : la dialog clé API reste ouverte, toast d'erreur, aucun changement côté store si la requête échoue.
  • Clé invalide en conditions réelles : après PATCH réussi, parcourir la connexion avec une mauvaise clé → badge Erreur, markConnectionAsError en base (sans rollback du PATCH).
  • Renommage + rotation simultanés via Swagger / curl : un seul PATCH met à jour les deux champs.
  • Garde adminGuard : rechargement direct (F5) sur /connections avec session expirée → redirection vers /upload sans erreur Vue Router non gérée ; rechargement avec getMe qui timeout → même redirection.
  • Routes ajoutées au passage (/workflows, sources/workflows) : sidebar admin affiche Dépôt / Sources / Workflows / Connexions ; highlight actif sur /workflows.
  • Connection Browse (fiches) : sur /connections/:uuid/browse, ressource Fiches, après sélection d'une fiche et chargement des soumissions, le bouton Créer une source est visible.
  • Redirection fiches : clic sur Créer une source depuis card/sources/create?connectionUuid=...&cardUuid=...&cardTitle=....
  • Wizard source (card) : avec query cardUuid, arrivée directe sur l'étape Nommer (parité avec formUuid).
  • Preview + submit card : GET /sources/api-preview avec cardUuid, puis POST /sources avec cardUuid et apiResourceType='card'.

7. Tests associés

Backend (unitaires)

back/src/api/connections/connections.service.spec.ts — bloc describe('updateConnection') :

  • Renommage seul (trim, pas d'encrypt).
  • Scope admin → findFirst inclut enterprise_id.
  • Scope super-admin → pas de filtre entreprise.
  • Rotation de clé seule : encrypt appelé, payload Prisma complet (api_key_encrypted, api_key_iv, api_key_prefix, status=ACTIVE, error_message=null, last_validated_at instance de Date récente), pas d'appel fetch / /connection-check.
  • Renommage + rotation combinés : un seul update, payload Prisma contenant les deux blocs.
  • BadRequestException si DTO vide.
  • BadRequestException si name et apiKey blancs après trim.
  • Connexion introuvable → ApiConnectionNotFoundException, pas d'effet de bord.
  • markConnectionAsError : mise à jour status=ERROR si connexion trouvée ; no-op si introuvable.
  • back/src/api/sources/sources.service.spec.ts :
    • createSource API depuis formulaire (historique).
    • createSource API depuis fiche : branche loadCardSubmissions couverte.
    • getApiFormPreview avec apiResourceType='card' : branche loadCardSubmissions couverte.

Frontend (unitaires)

  • front/src/__tests__/stores/connections-store.spec.js — bloc updateConnection :

    • Sans connectionUuid → retourne null, n'appelle pas api.patch.
    • apiKey seul → payload { apiKey }, mise à jour locale du apiKeyPrefix.
    • name + apiKey → payload combiné.
    • Valeurs non-string (number, null) → filtrées (payload {}).
    • Connexion absente du state local → pas d'erreur, retour de la donnée serveur.
    • renamingConnectionUuid positionné pendant la requête, reset à null après.
    • Erreur API → flag remis à null via finally, liste locale non mutée.
  • front/src/__tests__/pages/connections/ConnectionsPage.spec.js :

    • Renommage : ouverture, validation (trim → Nouveau nom), annulation, échec → toast erreur.
    • Vide après trim → pas d'appel updateConnection.
    • Dialog clé API : ouverture, mise à jour réussie (payload { apiKey }, dialog fermée, toast succès), vide → pas d'appel, échec API simulé → dialog reste ouverte, annulation → fermeture sans appel.
    • Suppression : confirmation, refetch, toast succès.
  • front/src/__tests__/router/adminGuard.spec.js :

    • Autorise si isAdmin.
    • Redirige /upload si non-admin.
    • Charge le profil si user null.
    • Redirige /upload si getMe échoue.
  • front/src/__tests__/router/routes.spec.js : 18 routes enfants, ajout workflows (avec sourcesGuard) et redirect sources/workflows → /workflows.

  • front/src/__tests__/layouts/MainLayout.spec.js : sidebar admin = 4 liens (Dépôt / Sources / Workflows / Connexions), highlight /workflows.

  • front/src/__tests__/pages/connections/ConnectionBrowsePage.spec.js :

    • redirection Créer une source avec formUuid/formTitle (historique),
    • redirection Créer une source avec cardUuid/cardTitle (nouveau).
  • front/src/__tests__/pages/admin/sources/AdminSourceCreatePage.spec.js :

    • preview + submit en mode API formulaire (historique),
    • preview + submit en mode API fiche (cardUuid, cardTitle, apiResourceType='card').
  • front/src/__tests__/stores/source-store.spec.js :

    • getApiSourcePreview supporte formUuid et cardUuid,
    • createSource supporte payload API fiche.

Frontend (e2e Playwright)

front/e2e/pages/connections/connections.spec.js :

  • Bloc menu actions : présence de Consulter, Renommer, Modifier la clé API, Supprimer ; clic sur Consulter → redirection vers /connections/:uuid/browse.
  • Bloc modification clé API :
    • Succès : ouverture dialog, payload { apiKey: trimé }, toast « Clé API mise à jour », dialog fermée, nouveau préfixe affiché.
    • Bouton Enregistrer désactivé tant que la clé est vide.
    • Échec PATCH simulé (ex. message d'erreur API) : dialog reste ouverte, toast erreur.
    • Annuler → dialog fermée, aucun PATCH.

8. Notes d'implémentation

  • Endpoint unifié plutôt que PATCH /connections/:uuid/api-key séparé : un seul handler, un seul DTO, une seule validation. Les UI front (édition inline / dialog) restent indépendantes côté UX.
  • La dialog clé API est délibérément différente de l'édition inline du nom : action sensible (change secrets chiffrés et statut affiché), un dialog modal réduit le risque de modification accidentelle. La validité réelle de la clé n'est pas testée à l'enregistrement : premier signal lors d'un parcours ou d'une source API.
  • Le statut ACTIVE reset sur rotation réussie permet de réparer une connexion auto-marquée ERROR (par markConnectionAsError lors d'un appel API qui aurait reçu un 401) sans devoir recréer la connexion ni perdre les sources API_FORM rattachées.
  • La migration de adminGuard en async (ancienne signature (to, from, next) => next()) a été nécessaire pour résoudre un warning Vue Router 4 (« next is deprecated, return a value instead ») et pour pouvoir appeler await getMe() avant la décision de routage. Tests adminGuard.spec.js mis à jour en conséquence.
  • L'ajout des routes workflows / sources/workflows n'appartient pas au scope BDD-125 stricto sensu, mais a été synchronisé avec develop au passage ; les specs routes.spec.js et MainLayout.spec.js ont été ajustés pour refléter ces 2 routes supplémentaires.
  • Extension card implémentée sans migration : les données restent dans le modèle API_FORM existant.
  • GET /sources/api-preview impose un XOR strict entre formUuid et cardUuid pour éviter les ambiguïtés.
  • UX harmonisée : un seul bouton Créer une source dans ConnectionBrowsePage pour formulaires et fiches.

9. Hors périmètre

  • Rafraîchissement automatique d'une source API_FORM après rotation de clé (re-sync des réponses) → BDD-119.
  • Audit log des modifications de connexion (qui a changé la clé, quand) → ticket dédié si besoin produit.
  • Modification de l'URL de base d'une connexion (baseUrl) : volontairement non exposée — une changement d'URL est considéré comme une nouvelle connexion (création + migration manuelle des sources).
  • Modification du type (FORMULAIRES → autre) : non applicable tant qu'il n'existe qu'un seul ApiConnectionType.