diff --git a/MISSION_INTEGRATION_IDS_FIX.md b/MISSION_INTEGRATION_IDS_FIX.md new file mode 100644 index 00000000..0d26fb56 --- /dev/null +++ b/MISSION_INTEGRATION_IDS_FIX.md @@ -0,0 +1,308 @@ +# Fix : Sauvegarde des IDs d'Intégration N8N + +## 🔍 Problème Identifié + +Lors de la suppression d'une mission, le webhook N8N reçoit des IDs vides : + +```json +{ + "repoName": "", + "leantimeProjectId": 0, + "documentationCollectionId": "", + "rocketchatChannelId": "" +} +``` + +**Cause** : Les IDs retournés par N8N lors de la création des intégrations n'étaient **jamais sauvegardés en base**. + +### Workflow Problématique + +``` +1. Frontend → POST /api/missions + ↓ +2. Backend crée mission en Prisma + ↓ +3. Backend upload fichiers Minio + ↓ +4. Backend → POST N8N webhook (mission-created) + ↓ +5. N8N crée intégrations (Gitea, Leantime, Outline, RocketChat) + ↓ +6. N8N → POST /mission-created ❌ ENDPOINT N'EXISTAIT PAS + ↓ +7. IDs jamais sauvegardés ❌ +``` + +### Conséquence + +Lors de la suppression : +- Les IDs sont `null` en base +- On envoie des valeurs vides à N8N +- N8N ne peut pas supprimer/fermer les intégrations +- Les ressources externes restent orphelines + +--- + +## ✅ Solution Implémentée + +### 1. Endpoint `/mission-created` Créé + +**Fichier** : `app/api/missions/mission-created/route.ts` + +**Fonctionnalités** : +- ✅ Reçoit les IDs des intégrations créées par N8N +- ✅ Vérifie l'API key (`x-api-key` header) +- ✅ Trouve la mission par `name` + `creatorId` +- ✅ Met à jour la mission avec les IDs +- ✅ Mappe correctement les champs : + - `gitRepoUrl` → `giteaRepositoryUrl` + - `documentationCollectionId` → `outlineCollectionId` + - `rocketchatChannelId` → `rocketChatChannelId` + - `leantimeProjectId` → `leantimeProjectId` (converti en string) + +### 2. Format des Données + +**N8N envoie** : +```json +{ + "name": "Mission Example", + "creatorId": "user-id", + "gitRepoUrl": "https://gite.slm-lab.net/alma/mission-example", + "leantimeProjectId": "123", + "documentationCollectionId": "collection-456", + "rocketchatChannelId": "channel-789" +} +``` + +**Endpoint sauvegarde** : +```typescript +{ + giteaRepositoryUrl: "https://gite.slm-lab.net/alma/mission-example", + leantimeProjectId: "123", + outlineCollectionId: "collection-456", + rocketChatChannelId: "channel-789" +} +``` + +### 3. Sécurité + +- ✅ Vérification de l'API key (`x-api-key` header) +- ✅ Validation des champs requis (`name`, `creatorId`) +- ✅ Gestion d'erreurs complète +- ✅ Logging détaillé pour debugging + +--- + +## 🔄 Nouveau Workflow + +``` +1. Frontend → POST /api/missions + ↓ +2. Backend crée mission en Prisma + ↓ +3. Backend upload fichiers Minio + ↓ +4. Backend → POST N8N webhook (mission-created) + ↓ +5. N8N crée intégrations (Gitea, Leantime, Outline, RocketChat) + ↓ +6. N8N → POST /mission-created ✅ ENDPOINT EXISTE MAINTENANT + ↓ +7. Backend sauvegarde les IDs ✅ + ↓ +8. Mission complète avec tous les IDs ✅ +``` + +**Lors de la suppression** : +``` +1. Frontend → DELETE /api/missions/[id] + ↓ +2. Backend récupère mission (avec IDs sauvegardés) + ↓ +3. Backend extrait/mappe les données + ↓ +4. Backend → POST N8N webhook (mission-delete) + ↓ +5. N8N reçoit les IDs ✅ + ↓ +6. N8N supprime/ferme les intégrations ✅ +``` + +--- + +## 📋 Format de Requête + +### POST /api/missions/mission-created + +**Headers** : +``` +Content-Type: application/json +x-api-key: {N8N_API_KEY} +Authorization: Bearer {keycloak_token} (optionnel) +``` + +**Body** : +```json +{ + "name": "Mission Example", + "creatorId": "user-uuid", + "gitRepoUrl": "https://gite.slm-lab.net/alma/mission-example", + "leantimeProjectId": "123", + "documentationCollectionId": "collection-456", + "rocketchatChannelId": "channel-789", + "niveau": "default", + "intention": "...", + "description": "...", + "donneurDOrdre": "...", + "projection": "...", + "missionType": "remote" +} +``` + +**Réponse Succès** (200) : +```json +{ + "success": true, + "message": "Mission updated successfully", + "mission": { + "id": "mission-uuid", + "name": "Mission Example", + "giteaRepositoryUrl": "https://gite.slm-lab.net/alma/mission-example", + "leantimeProjectId": "123", + "outlineCollectionId": "collection-456", + "rocketChatChannelId": "channel-789" + } +} +``` + +**Réponse Erreur** (400/404/500) : +```json +{ + "error": "Error message", + "details": "Detailed error information" +} +``` + +--- + +## ⚠️ Missions Existantes + +**Problème** : Les missions créées avant cette correction n'ont pas leurs IDs sauvegardés. + +**Solutions possibles** : + +### Option 1 : Migration Manuelle +Pour chaque mission existante, récupérer les IDs depuis les services externes et les mettre à jour manuellement. + +### Option 2 : Script de Migration +Créer un script qui : +1. Liste toutes les missions sans IDs +2. Interroge les services externes (si possible) +3. Met à jour les missions + +### Option 3 : Re-création +Supprimer et recréer les missions (si acceptable). + +**Recommandation** : Option 1 pour les missions critiques, Option 2 pour un grand nombre. + +--- + +## 🧪 Tests + +### Test 1 : Création de Mission + +1. Créer une nouvelle mission via le frontend +2. Vérifier que N8N appelle `/mission-created` +3. Vérifier que la mission en base a les IDs sauvegardés : + ```sql + SELECT id, name, giteaRepositoryUrl, leantimeProjectId, + outlineCollectionId, rocketChatChannelId + FROM Mission + WHERE name = 'Mission Test'; + ``` + +### Test 2 : Suppression de Mission + +1. Supprimer une mission avec IDs sauvegardés +2. Vérifier que N8N reçoit les IDs : + ```json + { + "repoName": "mission-example", + "leantimeProjectId": 123, + "documentationCollectionId": "collection-456", + "rocketchatChannelId": "channel-789" + } + ``` +3. Vérifier que N8N supprime/ferme les intégrations + +### Test 3 : API Key + +1. Appeler `/mission-created` sans `x-api-key` → 401 +2. Appeler avec mauvais `x-api-key` → 401 +3. Appeler avec bon `x-api-key` → 200 + +--- + +## 📝 Logs à Surveiller + +### Création + +``` +=== Mission Created Webhook Received === +Received mission-created data: { ... } +Found mission: { id: "...", name: "..." } +Updating giteaRepositoryUrl: ... +Updating leantimeProjectId: ... +Mission updated successfully: { ... } +``` + +### Suppression + +``` +=== Starting N8N Deletion Workflow === +Extracted repo name from URL: { url: "...", repoName: "..." } +Sending deletion data to N8N: { ... } +N8N Deletion Workflow Result: { success: true, ... } +``` + +--- + +## 🔧 Configuration Requise + +### Variables d'Environnement + +```env +N8N_API_KEY=your-api-key-here +NEXT_PUBLIC_API_URL=https://hub.slm-lab.net +``` + +### N8N Workflow + +Le workflow N8N doit appeler : +- **URL** : `{{ MISSION_API_URL }}/mission-created` +- **Méthode** : POST +- **Headers** : + - `Content-Type: application/json` + - `x-api-key: {{ N8N_API_KEY }}` + - `Authorization: Bearer {{ Keycloak Token }}` (optionnel) + +--- + +## ✅ Checklist + +- [x] Endpoint `/mission-created` créé +- [x] Vérification API key implémentée +- [x] Mapping des champs correct +- [x] Gestion d'erreurs complète +- [x] Logging détaillé +- [ ] Tests manuels effectués +- [ ] Migration des missions existantes (si nécessaire) +- [ ] Documentation N8N mise à jour + +--- + +**Date de correction** : $(date) +**Version** : 1.0 +**Fichiers modifiés** : +- `app/api/missions/mission-created/route.ts` (nouveau) + diff --git a/N8N_COMPLETE_WORKFLOW_MAPPING.md b/N8N_COMPLETE_WORKFLOW_MAPPING.md new file mode 100644 index 00000000..cfddacb6 --- /dev/null +++ b/N8N_COMPLETE_WORKFLOW_MAPPING.md @@ -0,0 +1,673 @@ +# Mapping Complet N8N - Création et Suppression de Mission + +## 📋 Vue d'Ensemble + +Ce document décrit le mapping complet entre notre API et les workflows N8N pour la création et la suppression de missions, basé sur les workflows réels partagés. + +--- + +## 🔄 Workflow de Création - NeahMissionCreate + +### Structure du Workflow + +``` +Webhook (mission-created) + ↓ +Process Mission Data + ↓ +Get Keycloak Token + ↓ +Process Token + ↓ +Debug Service Data + ↓ +Merge Paths + ↓ +IF Run Integrations + ├─ IF Needs Git Repository + │ ├─ Create Git Repository (si Gite ou Calcul) + │ ├─ Create Readme + │ └─ Git Wiki + ├─ Create Documentation Collection + ├─ Create Leantime Project + │ └─ Leantime Avatar + └─ Create RocketChat Channel + ↓ +Combine Results + ↓ +Save Mission To API (POST /mission-created) + ↓ +Process Results + ↓ +Respond To Webhook +``` + +### Données Envoyées par Notre API → N8N + +**Endpoint** : `POST https://brain.slm-lab.net/webhook/mission-created` + +**Format** : +```typescript +{ + name: string, + oddScope: string[], + niveau: string, + intention: string, + missionType: string, + donneurDOrdre: string, + projection: string, + services: string[], + participation: string, + profils: string[], + guardians: { + "gardien-temps": userId, + "gardien-parole": userId, + "gardien-memoire": userId + }, + volunteers: string[], + creatorId: string, + logo: { + data: "data:image/png;base64,...", + name: string, + type: string + }, + attachments: Array<{ + data: "data:...;base64,...", + name: string, + type: string + }>, + config: { + N8N_API_KEY: string, + MISSION_API_URL: string + } +} +``` + +### Traitement N8N - Process Mission Data + +Le node "Process Mission Data" transforme les données en : + +```javascript +{ + missionOriginal: { ... }, // Données originales + missionProcessed: { + name: "Mission Example", + sanitizedName: "mission-example", // Nom nettoyé pour URLs + intention: "...", + description: "...", + startDate: "2024-01-01", + endDate: "2024-01-31", + missionType: "remote", + guardians: { ... }, + volunteers: [ ... ], + profils: [ ... ], + services: ["Gite", "ArtLab"], // Détermine quelles intégrations créer + clientId: 2, + rocketChatUsernames: [userId1, userId2, ...], // Gardiens + volontaires + logo: { data: "...", name: "...", type: "..." }, + attachments: [ ... ] + }, + config: { + GITEA_API_URL: "https://gite.slm-lab.net/api/v1", + GITEA_API_TOKEN: "...", + GITEA_OWNER: "alma", + LEANTIME_API_URL: "https://agilite.slm-lab.net", + LEANTIME_API_TOKEN: "...", + ROCKETCHAT_API_URL: "https://parole.slm-lab.net/", + ROCKETCHAT_AUTH_TOKEN: "...", + ROCKETCHAT_USER_ID: "...", + OUTLINE_API_URL: "https://chapitre.slm-lab.net/api", + OUTLINE_API_TOKEN: "...", + MISSION_API_URL: "https://hub.slm-lab.net", + // ... autres configs + }, + creatorId: "user-id" +} +``` + +### Intégrations Créées par N8N + +#### 1. Gitea Repository (Conditionnel) + +**Condition** : `services.includes('Gite') || services.includes('Calcul')` + +**Node** : "Create Git Repository" +- **Méthode** : POST +- **URL** : `{{ GITEA_API_URL }}/user/repos` +- **Body** : + ```json + { + "name": "{{ sanitizedName }}", + "private": true, + "auto_init": true, + "avatar_url": "{{ logo.data }}" + } + ``` +- **Résultat** : `{ html_url: "https://gite.slm-lab.net/alma/mission-example" }` + +**Actions supplémentaires** : +- Create Readme : Crée un document README dans Outline +- Git Wiki : Configure le wiki externe du repo vers Outline + +#### 2. Leantime Project + +**Node** : "Create Leantime Project" +- **Méthode** : POST +- **URL** : `{{ LEANTIME_API_URL }}/api/jsonrpc` +- **Body** : + ```json + { + "method": "leantime.rpc.Projects.Projects.addProject", + "jsonrpc": "2.0", + "id": 1, + "params": { + "values": { + "name": "{{ name }}", + "clientId": {{ clientId }}, + "details": "{{ intention }}", + "type": "project", + "start": "{{ startDate }}", + "end": "{{ endDate }}", + "status": "open", + "psettings": "restricted", + "avatar": "{{ logo.data }}" + } + } + } + ``` +- **Résultat** : `{ result: [projectId] }` (array avec 1 élément) + +**Action supplémentaire** : +- Leantime Avatar : Met à jour l'avatar du projet + +#### 3. Outline Collection + +**Node** : "Create Documentation Collection" +- **Méthode** : POST +- **URL** : `{{ OUTLINE_API_URL }}/api/collections.create` +- **Body** : + ```json + { + "name": "{{ sanitizedName }}", + "description": "{{ description }}", + "permission": "read", + "private": true + } + ``` +- **Résultat** : `{ data: { id: "collection-id", url: "/collection/..." } }` + +#### 4. RocketChat Channel + +**Node** : "Create RocketChat Channel" +- **Méthode** : POST +- **URL** : `{{ ROCKETCHAT_API_URL }}/api/v1/channels.create` +- **Body** : + ```json + { + "name": "{{ sanitizedName }}", + "members": [{{ rocketChatUsernames }}], + "readOnly": false, + "avatarUrl": "{{ logo.data }}" + } + ``` +- **Résultat** : `{ channel: { _id: "channel-id", ... } }` + +### Save Mission To API - Retour vers Notre API + +**Node** : "Save Mission To API" +- **Méthode** : POST +- **URL** : `{{ MISSION_API_URL }}/mission-created` +- **Headers** : + - `Content-Type: application/json` + - `Authorization: Bearer {{ Keycloak Token }}` + - `x-api-key: {{ N8N_API_KEY }}` +- **Body** : + ```json + { + "name": "{{ name }}", + "niveau": "{{ niveau }}", + "intention": "{{ intention }}", + "description": "{{ description }}", + "gitRepoUrl": "{{ gitRepo.html_url }}", + "leantimeProjectId": "{{ leantimeProject.result[0] }}", + "documentationCollectionId": "{{ docCollection.data.id }}", + "rocketchatChannelId": "{{ rocketChatChannel.channel._id }}", + "donneurDOrdre": "{{ donneurDOrdre }}", + "projection": "{{ projection }}", + "missionType": "{{ missionType }}", + "creatorId": "{{ creatorId }}" + } + ``` + +**⚠️ IMPORTANT** : Cet endpoint `/mission-created` n'existe **PAS** actuellement dans notre codebase. Il devrait : +1. Recevoir les IDs des intégrations créées +2. Mettre à jour la mission en base avec ces IDs +3. Mapper les champs : + - `gitRepoUrl` → `giteaRepositoryUrl` + - `documentationCollectionId` → `outlineCollectionId` + - `rocketchatChannelId` → `rocketChatChannelId` + +--- + +## 🗑️ Workflow de Suppression - NeahMissionDelete_Pro + +### Structure du Workflow + +``` +Webhook Delete (mission-delete) + ↓ +Process Delete Data + ↓ +Get Keycloak Token + ↓ +[En parallèle] + ├─ Delete Gitea Repo + ├─ Close Leantime Project + ├─ Delete Outline Collection + └─ Close RocketChat Channel + ↓ +Combine Results + ↓ +Save Deletion To API (POST /mission-deleted) +``` + +### Données Envoyées par Notre API → N8N + +**Endpoint** : `POST https://brain.slm-lab.net/webhook-test/mission-delete` + +**Format** : +```typescript +{ + missionId: string, + name: string, + repoName: string, // ✅ Extrait de giteaRepositoryUrl + leantimeProjectId: number | 0, // ✅ Converti en number + documentationCollectionId: string, // ✅ Mappé depuis outlineCollectionId + rocketchatChannelId: string, // ✅ Mappé depuis rocketChatChannelId + // Champs originaux pour référence + giteaRepositoryUrl: string | null, + outlineCollectionId: string | null, + rocketChatChannelId: string | null, + penpotProjectId: string | null, + config: { + N8N_API_KEY: string, + MISSION_API_URL: string + } +} +``` + +### Traitement N8N - Process Delete Data + +Le node "Process Delete Data" transforme les données en : + +```javascript +{ + missionData: { + repoName: input.repoName || '', + leantimeId: input.leantimeProjectId || 0, + collectionId: input.documentationCollectionId || '', + rocketChatRoomId: input.rocketchatChannelId || '' + }, + config: { + GITEA_API_URL: "https://gite.slm-lab.net/api/v1", + GITEA_API_TOKEN: "...", + GITEA_OWNER: "alma", + LEANTIME_API_URL: "https://agilite.slm-lab.net", + LEANTIME_API_TOKEN: "...", + ROCKETCHAT_API_URL: "https://parole.slm-lab.net/", + ROCKETCHAT_AUTH_TOKEN: "...", + ROCKETCHAT_USER_ID: "...", + OUTLINE_API_URL: "https://chapitre.slm-lab.net/api", + OUTLINE_API_TOKEN: "...", + MISSION_API_URL: "https://hub.slm-lab.net", + KEYCLOAK_BASE_URL: "https://connect.slm-lab.net", + KEYCLOAK_REALM: "cercle", + KEYCLOAK_CLIENT_ID: "lab", + KEYCLOAK_CLIENT_SECRET: "..." + } +} +``` + +### Actions de Suppression N8N + +#### 1. Delete Gitea Repo + +**Node** : "Delete Gitea Repo" +- **Méthode** : DELETE +- **URL** : `{{ GITEA_API_URL }}/repos/{{ GITEA_OWNER }}/{{ repoName }}` +- **Headers** : `Authorization: token {{ GITEA_API_TOKEN }}` +- **ContinueOnFail** : `true` +- **Résultat attendu** : Status 204 = succès + +#### 2. Close Leantime Project + +**Node** : "Close Leantime Project" +- **Méthode** : POST +- **URL** : `{{ LEANTIME_API_URL }}/api/jsonrpc` +- **Body** : + ```json + { + "method": "leantime.rpc.Projects.Projects.patch", + "jsonrpc": "2.0", + "id": 1, + "params": { + "id": {{ leantimeId }}, + "params": { "status": "closed" } + } + } + ``` +- **ContinueOnFail** : `true` +- **Note** : Le projet est **fermé** (status: "closed"), pas supprimé + +#### 3. Delete Outline Collection + +**Node** : "Delete Outline Collection" +- **Méthode** : POST +- **URL** : `{{ OUTLINE_API_URL }}/api/collections.delete` +- **Body** : `{ "id": "{{ collectionId }}" }` +- **ContinueOnFail** : `true` +- **Résultat attendu** : Status 200 = succès + +#### 4. Close RocketChat Channel + +**Node** : "Close RocketChat Channel" +- **Méthode** : POST +- **URL** : `{{ ROCKETCHAT_API_URL }}/api/v1/channels.close` +- **Body** : `{ "roomId": "{{ rocketChatRoomId }}" }` +- **ContinueOnFail** : `true` +- **Note** : Le canal est **fermé**, pas supprimé + +### Combine Results + +Le node "Combine Results" combine les résultats : + +```javascript +{ + status: "deleted", + timestamp: "2024-01-01T12:00:00.000Z", + details: { + gitea: true || "already_deleted", + leantime: true || false, + outline: true || false, + rocketchat: true || false + } +} +``` + +### Save Deletion To API - Retour vers Notre API + +**Node** : "Save Deletion To API" +- **Méthode** : POST +- **URL** : `{{ MISSION_API_URL }}/mission-deleted` +- **Headers** : + - `Authorization: Bearer {{ Keycloak Token }}` +- **Body** : + ```json + { + "status": "archived", + "results": { + "gitea": true, + "leantime": true, + "outline": true, + "rocketchat": true + } + } + ``` + +**⚠️ IMPORTANT** : Cet endpoint `/mission-deleted` n'existe **PAS** actuellement dans notre codebase. Il pourrait servir à : +1. Confirmer la suppression +2. Logger les résultats +3. Nettoyer des données supplémentaires si nécessaire + +--- + +## 📊 Mapping Complet des Champs + +### Création (Notre API → N8N → Retour) + +| Notre Base | Envoyé à N8N | N8N Crée | Retour N8N | Stocké en Base | +|-----------|--------------|----------|------------|----------------| +| - | `name` | - | `name` | `name` | +| - | `services` | Détermine intégrations | - | `services` | +| - | `logo.data` | Avatar/Logo | - | `logo` (path) | +| - | - | Gitea Repo | `gitRepoUrl` | `giteaRepositoryUrl` | +| - | - | Leantime Project | `leantimeProjectId` | `leantimeProjectId` | +| - | - | Outline Collection | `documentationCollectionId` | `outlineCollectionId` | +| - | - | RocketChat Channel | `rocketchatChannelId` | `rocketChatChannelId` | + +### Suppression (Notre Base → N8N) + +| Notre Base | Extrait/Transformé | Envoyé à N8N | N8N Attend | +|-----------|-------------------|--------------|------------| +| `giteaRepositoryUrl` | Extraction nom | `repoName` | `repoName` | +| `leantimeProjectId` | Converti en number | `leantimeProjectId` | `leantimeId` | +| `outlineCollectionId` | Direct | `documentationCollectionId` | `collectionId` | +| `rocketChatChannelId` | Direct | `rocketchatChannelId` | `rocketChatRoomId` | + +--- + +## 🔧 Transformations Clés + +### 1. Extraction du Nom du Repository Gitea + +**Problème** : Notre base stocke l'URL complète, N8N attend le nom seul + +**Solution** : +```typescript +// Format: https://gite.slm-lab.net/alma/mission-example +// ou: https://gite.slm-lab.net/api/v1/repos/alma/mission-example + +let repoName = ''; +if (giteaRepositoryUrl) { + try { + const url = new URL(giteaRepositoryUrl); + const pathParts = url.pathname.split('/').filter(Boolean); + repoName = pathParts[pathParts.length - 1] || ''; + } catch (error) { + const match = giteaRepositoryUrl.match(/\/([^\/]+)\/?$/); + repoName = match ? match[1] : ''; + } +} +``` + +### 2. Mapping des Champs + +**Création** : +- N8N retourne `gitRepoUrl` → On stocke `giteaRepositoryUrl` +- N8N retourne `documentationCollectionId` → On stocke `outlineCollectionId` +- N8N retourne `rocketchatChannelId` → On stocke `rocketChatChannelId` + +**Suppression** : +- On stocke `giteaRepositoryUrl` → On envoie `repoName` (extrait) +- On stocke `outlineCollectionId` → On envoie `documentationCollectionId` +- On stocke `rocketChatChannelId` → On envoie `rocketchatChannelId` + +### 3. Conversion de Types + +**Leantime Project ID** : +- Stocké en base : `string | null` +- Envoyé à N8N : `number | 0` (converti) +- N8N attend : `number` (dans `leantimeId`) + +--- + +## ⚠️ Endpoints Manquants + +### 1. POST /mission-created + +**Rôle** : Recevoir les IDs des intégrations créées par N8N + +**Format attendu** : +```typescript +POST /mission-created +Headers: { + Authorization: "Bearer {keycloak_token}", + x-api-key: "{N8N_API_KEY}" +} +Body: { + name: string, + niveau: string, + intention: string, + description: string, + gitRepoUrl: string, // À mapper vers giteaRepositoryUrl + leantimeProjectId: string, // À mapper vers leantimeProjectId + documentationCollectionId: string, // À mapper vers outlineCollectionId + rocketchatChannelId: string, // À mapper vers rocketChatChannelId + donneurDOrdre: string, + projection: string, + missionType: string, + creatorId: string +} +``` + +**Action requise** : +1. Trouver la mission par `name` + `creatorId` +2. Mettre à jour avec les IDs retournés +3. Mapper les champs correctement + +### 2. POST /mission-deleted + +**Rôle** : Confirmer la suppression (optionnel) + +**Format attendu** : +```typescript +POST /mission-deleted +Headers: { + Authorization: "Bearer {keycloak_token}" +} +Body: { + status: "archived", + results: { + gitea: boolean, + leantime: boolean, + outline: boolean, + rocketchat: boolean + } +} +``` + +**Action requise** : +- Logger les résultats +- Potentiellement nettoyer des données supplémentaires + +--- + +## 🔄 Flow Complet - Vue d'Ensemble + +### Création + +``` +1. Frontend → POST /api/missions + ↓ +2. Backend crée mission en Prisma + ↓ +3. Backend upload fichiers Minio + ↓ +4. Backend → POST N8N webhook (mission-created) + ↓ +5. N8N crée intégrations (Gitea, Leantime, Outline, RocketChat) + ↓ +6. N8N → POST /mission-created (⚠️ endpoint manquant) + ↓ +7. Backend met à jour mission avec IDs (⚠️ non implémenté) +``` + +### Suppression + +``` +1. Frontend → DELETE /api/missions/[id] + ↓ +2. Backend récupère mission + ↓ +3. Backend extrait/mappe les données + ↓ +4. Backend → POST N8N webhook (mission-delete) + ↓ +5. N8N supprime/ferme intégrations + ↓ +6. N8N → POST /mission-deleted (⚠️ endpoint manquant) + ↓ +7. Backend supprime logo Minio + ↓ +8. Backend supprime attachments Minio + ↓ +9. Backend supprime mission Prisma (CASCADE) +``` + +--- + +## 📝 Notes Importantes + +### 1. Noms de Champs Incohérents + +- **Création** : N8N retourne `gitRepoUrl`, `documentationCollectionId`, `rocketchatChannelId` +- **Suppression** : N8N attend `repoName`, `documentationCollectionId`, `rocketchatChannelId` +- **Notre Base** : Stocke `giteaRepositoryUrl`, `outlineCollectionId`, `rocketChatChannelId` + +**Solution** : Mapping cohérent dans les deux sens + +### 2. Endpoint /mission-created Manquant + +Actuellement, les IDs retournés par N8N ne sont **PAS** sauvegardés en base. Il faudrait : +- Créer l'endpoint `/mission-created` +- Trouver la mission (par `name` + `creatorId` ou `missionId`) +- Mettre à jour avec les IDs + +### 3. Services Conditionnels + +- **Gitea** : Créé seulement si `services.includes('Gite') || services.includes('Calcul')` +- **Leantime** : Toujours créé +- **Outline** : Toujours créé +- **RocketChat** : Toujours créé + +### 4. Gestion d'Erreurs + +- Tous les nodes N8N ont `continueOnFail: true` +- Les erreurs sont loggées mais n'arrêtent pas le workflow +- Les résultats indiquent quelles intégrations ont réussi/échoué + +--- + +## 🔍 Points de Debugging + +### Création + +1. **Vérifier données envoyées à N8N** : + ``` + Sending to N8N: { ... } + ``` + +2. **Vérifier réponse N8N** : + ``` + N8N Workflow Result: { success: true, results: {...} } + ``` + +3. **Vérifier endpoint /mission-created** : + - Doit recevoir les IDs + - Doit mettre à jour la mission + +### Suppression + +1. **Vérifier extraction repoName** : + ``` + Extracted repo name from URL: { url: "...", repoName: "..." } + ``` + +2. **Vérifier données envoyées à N8N** : + ``` + Sending deletion data to N8N: { ... } + ``` + +3. **Vérifier réponse N8N** : + ``` + N8N Deletion Workflow Result: { success: true, results: {...} } + ``` + +--- + +**Document généré le** : $(date) +**Version** : 1.0 +**Workflows N8N** : +- NeahMissionCreate (création) +- NeahMissionDelete_Pro (suppression) + diff --git a/app/api/missions/mission-created/route.ts b/app/api/missions/mission-created/route.ts new file mode 100644 index 00000000..d1e83999 --- /dev/null +++ b/app/api/missions/mission-created/route.ts @@ -0,0 +1,187 @@ +import { NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +/** + * POST /api/missions/mission-created + * + * Endpoint appelé par N8N après la création des intégrations externes. + * Reçoit les IDs des intégrations créées et met à jour la mission en base. + * + * Headers attendus: + * - Authorization: Bearer {keycloak_token} (optionnel, vérifié via x-api-key) + * - x-api-key: {N8N_API_KEY} + * + * Body attendu (format N8N): + * { + * name: string, + * creatorId: string, + * gitRepoUrl?: string, + * leantimeProjectId?: string, + * documentationCollectionId?: string, + * rocketchatChannelId?: string, + * // ... autres champs optionnels + * } + */ +export async function POST(request: Request) { + try { + console.log('=== Mission Created Webhook Received ==='); + + // Vérifier l'API key + const apiKey = request.headers.get('x-api-key'); + const expectedApiKey = process.env.N8N_API_KEY; + + if (!expectedApiKey) { + console.error('N8N_API_KEY not configured in environment'); + return NextResponse.json( + { error: 'Server configuration error' }, + { status: 500 } + ); + } + + if (apiKey !== expectedApiKey) { + console.error('Invalid API key:', { + received: apiKey ? 'present' : 'missing', + expected: expectedApiKey ? 'configured' : 'missing' + }); + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + const body = await request.json(); + console.log('Received mission-created data:', JSON.stringify(body, null, 2)); + + // Validation des champs requis + if (!body.name || !body.creatorId) { + console.error('Missing required fields:', { + hasName: !!body.name, + hasCreatorId: !!body.creatorId + }); + return NextResponse.json( + { error: 'Missing required fields: name and creatorId' }, + { status: 400 } + ); + } + + // Trouver la mission par name + creatorId + // On cherche la mission la plus récente avec ce nom et ce créateur + const mission = await prisma.mission.findFirst({ + where: { + name: body.name, + creatorId: body.creatorId + }, + orderBy: { + createdAt: 'desc' // Prendre la plus récente + } + }); + + if (!mission) { + console.error('Mission not found:', { + name: body.name, + creatorId: body.creatorId + }); + return NextResponse.json( + { error: 'Mission not found' }, + { status: 404 } + ); + } + + console.log('Found mission:', { + id: mission.id, + name: mission.name, + currentIntegrationIds: { + gitea: mission.giteaRepositoryUrl, + leantime: mission.leantimeProjectId, + outline: mission.outlineCollectionId, + rocketChat: mission.rocketChatChannelId + } + }); + + // Préparer les données de mise à jour + const updateData: { + giteaRepositoryUrl?: string | null; + leantimeProjectId?: string | null; + outlineCollectionId?: string | null; + rocketChatChannelId?: string | null; + } = {}; + + // Mapper les champs N8N vers notre schéma Prisma + if (body.gitRepoUrl !== undefined) { + updateData.giteaRepositoryUrl = body.gitRepoUrl || null; + console.log('Updating giteaRepositoryUrl:', body.gitRepoUrl); + } + + if (body.leantimeProjectId !== undefined) { + // N8N peut retourner un number, on le convertit en string + updateData.leantimeProjectId = body.leantimeProjectId + ? String(body.leantimeProjectId) + : null; + console.log('Updating leantimeProjectId:', updateData.leantimeProjectId); + } + + if (body.documentationCollectionId !== undefined) { + updateData.outlineCollectionId = body.documentationCollectionId || null; + console.log('Updating outlineCollectionId:', updateData.outlineCollectionId); + } + + if (body.rocketchatChannelId !== undefined) { + updateData.rocketChatChannelId = body.rocketchatChannelId || null; + console.log('Updating rocketChatChannelId:', updateData.rocketChatChannelId); + } + + // Vérifier qu'il y a au moins un champ à mettre à jour + if (Object.keys(updateData).length === 0) { + console.warn('No integration IDs to update'); + return NextResponse.json({ + message: 'Mission found but no integration IDs provided', + mission: { + id: mission.id, + name: mission.name + } + }); + } + + // Mettre à jour la mission + const updatedMission = await prisma.mission.update({ + where: { id: mission.id }, + data: updateData + }); + + console.log('Mission updated successfully:', { + id: updatedMission.id, + name: updatedMission.name, + updatedFields: Object.keys(updateData), + newIntegrationIds: { + gitea: updatedMission.giteaRepositoryUrl, + leantime: updatedMission.leantimeProjectId, + outline: updatedMission.outlineCollectionId, + rocketChat: updatedMission.rocketChatChannelId + } + }); + + return NextResponse.json({ + success: true, + message: 'Mission updated successfully', + mission: { + id: updatedMission.id, + name: updatedMission.name, + giteaRepositoryUrl: updatedMission.giteaRepositoryUrl, + leantimeProjectId: updatedMission.leantimeProjectId, + outlineCollectionId: updatedMission.outlineCollectionId, + rocketChatChannelId: updatedMission.rocketChatChannelId + } + }); + + } catch (error) { + console.error('Error in mission-created webhook:', error); + return NextResponse.json( + { + error: 'Failed to update mission', + details: error instanceof Error ? error.message : String(error) + }, + { status: 500 } + ); + } +} +