diff --git a/MISSION_CREATION_WORKFLOW_DETAILED.md b/MISSION_CREATION_WORKFLOW_DETAILED.md new file mode 100644 index 00000000..c7a91d55 --- /dev/null +++ b/MISSION_CREATION_WORKFLOW_DETAILED.md @@ -0,0 +1,982 @@ +# Workflow Détaillé : Création de Mission - Prisma, Minio et N8N + +## 📋 Vue d'Ensemble + +Ce document trace **chaque étape** du workflow de création de mission, depuis le formulaire frontend jusqu'aux intégrations externes via N8N, en passant par Prisma (base de données) et Minio (stockage de fichiers). + +--- + +## 🔄 Flux Complet - Vue d'Ensemble + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 1. FRONTEND - MissionsAdminPanel │ +│ - Validation des champs │ +│ - Préparation des données (base64 pour fichiers) │ +│ - POST /api/missions │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 2. BACKEND - POST /api/missions │ +│ ├─ Authentification (NextAuth) │ +│ ├─ Validation des champs requis │ +│ ├─ STEP 1: Prisma.mission.create() │ +│ ├─ STEP 2: Prisma.missionUser.createMany() │ +│ ├─ STEP 3: Upload Logo → Minio │ +│ ├─ STEP 4: Upload Attachments → Minio │ +│ ├─ STEP 5: Vérification fichiers Minio │ +│ └─ STEP 6: N8N Workflow Trigger │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 3. N8N WORKFLOW │ +│ - Création Leantime Project │ +│ - Création Outline Collection │ +│ - Création RocketChat Channel │ +│ - Création Gitea Repository │ +│ - Création Penpot Project │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 📝 ÉTAPE 1 : Frontend - Préparation des Données + +### Fichier : `components/missions/missions-admin-panel.tsx` + +### 1.1 Validation (`validateMission()`) + +**Lignes 369-397** + +```typescript +const validateMission = () => { + const requiredFields = { + name: !!missionData.name, + oddScope: Array.isArray(missionData.oddScope) && missionData.oddScope.length > 0, + niveau: !!missionData.niveau, + intention: !!missionData.intention, + missionType: !!missionData.missionType, + donneurDOrdre: !!missionData.donneurDOrdre, + projection: !!missionData.projection, + participation: !!missionData.participation, + gardiens: gardienDuTemps !== null && + gardienDeLaParole !== null && + gardienDeLaMemoire !== null + }; + + // Vérifie que tous les champs requis sont remplis + // Retourne false si un champ manque +} +``` + +**Champs requis** : +- ✅ `name` : Nom de la mission +- ✅ `oddScope` : Array avec au moins 1 ODD +- ✅ `niveau` : A/B/C/S +- ✅ `intention` : Description +- ✅ `missionType` : remote/onsite/hybrid +- ✅ `donneurDOrdre` : individual/group/organization +- ✅ `projection` : short/medium/long +- ✅ `participation` : volontaire/cooptation +- ✅ `gardiens` : Les 3 gardiens doivent être assignés + +### 1.2 Préparation des Données (`handleSubmitMission()`) + +**Lignes 400-460** + +```typescript +const handleSubmitMission = async () => { + // 1. Validation + if (!validateMission()) return; + + // 2. Préparation des gardiens + const guardians = { + "gardien-temps": gardienDuTemps, + "gardien-parole": gardienDeLaParole, + "gardien-memoire": gardienDeLaMemoire + }; + + // 3. Construction de l'objet de soumission + const missionSubmitData = { + ...missionData, // name, oddScope, niveau, intention, etc. + services: selectedServices, // ["Gite", "ArtLab", "Calcul"] + profils: selectedProfils, // ["DataIntelligence", "Expression", ...] + guardians, // { "gardien-temps": userId, ... } + volunteers: volontaires, // [userId1, userId2, ...] + logo: missionData.logo // { data: "data:image/png;base64,...", name, type } + }; + + // 4. Envoi à l'API + const response = await fetch('/api/missions', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(missionSubmitData) + }); +} +``` + +**Format du logo** (si présent) : +```typescript +logo: { + data: "data:image/png;base64,iVBORw0KGgoAAAANS...", // Base64 avec préfixe + name: "logo.png", + type: "image/png" +} +``` + +**Format des attachments** (si présents) : +```typescript +attachments: [ + { + data: "data:application/pdf;base64,JVBERi0xLjQKJe...", + name: "document.pdf", + type: "application/pdf" + } +] +``` + +--- + +## 🗄️ ÉTAPE 2 : Backend - POST /api/missions + +### Fichier : `app/api/missions/route.ts` + +### 2.1 Authentification et Validation + +**Lignes 205-224** + +```typescript +export async function POST(request: Request) { + // 1. Vérification de l'authentification + const { authorized, userId } = await checkAuth(request); + if (!authorized || !userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // 2. Parsing du body + const body = await request.json(); + + // 3. Validation minimale (name et oddScope requis) + if (!body.name || !body.oddScope) { + return NextResponse.json({ + error: 'Missing required fields', + missingFields: ['name', 'oddScope'].filter(field => !body[field]) + }, { status: 400 }); + } +} +``` + +### 2.2 STEP 1 : Création de la Mission en Base de Données + +**Lignes 226-248** + +```typescript +// Préparation des données pour Prisma +const missionData = { + name: body.name, + oddScope: body.oddScope, // Array de strings + niveau: body.niveau, + intention: body.intention, + missionType: body.missionType, + donneurDOrdre: body.donneurDOrdre, + projection: body.projection, + services: body.services, // Array de strings + profils: body.profils, // Array de strings + participation: body.participation, + creatorId: userId, // ID de l'utilisateur connecté + logo: null, // Sera mis à jour après upload +}; + +// Création en base de données +const mission = await prisma.mission.create({ + data: missionData +}); + +// Résultat : mission.id est maintenant disponible +// Exemple : mission.id = "abc-123-def-456" +``` + +**Schéma Prisma** : +```prisma +model Mission { + id String @id @default(uuid()) + name String + logo String? // null pour l'instant + oddScope String[] + niveau String + intention String + missionType String + donneurDOrdre String + projection String + services String[] + participation String? + profils String[] + creatorId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + // Relations + creator User @relation(...) + missionUsers MissionUser[] + attachments Attachment[] +} +``` + +**Points importants** : +- ✅ La mission est créée **AVANT** l'upload des fichiers +- ✅ Le `mission.id` est généré automatiquement (UUID) +- ✅ Le champ `logo` est `null` pour l'instant +- ✅ Tous les champs sont sauvegardés sauf les fichiers + +### 2.3 STEP 2 : Création des MissionUsers (Gardiens + Volontaires) + +**Lignes 250-283** + +```typescript +// Préparation du tableau de MissionUsers +const missionUsers = []; + +// 2.1 Ajout des gardiens +if (body.guardians) { + for (const [role, guardianId] of Object.entries(body.guardians)) { + if (guardianId) { + missionUsers.push({ + missionId: mission.id, // ID de la mission créée + userId: guardianId, // ID de l'utilisateur gardien + role: role // "gardien-temps", "gardien-parole", "gardien-memoire" + }); + } + } +} + +// 2.2 Ajout des volontaires +if (body.volunteers && body.volunteers.length > 0) { + for (const volunteerId of body.volunteers) { + missionUsers.push({ + missionId: mission.id, + userId: volunteerId, + role: 'volontaire' + }); + } +} + +// 2.3 Création en batch dans Prisma +if (missionUsers.length > 0) { + await prisma.missionUser.createMany({ + data: missionUsers + }); +} +``` + +**Schéma Prisma MissionUser** : +```prisma +model MissionUser { + id String @id @default(uuid()) + role String // 'gardien-temps', 'gardien-parole', 'gardien-memoire', 'volontaire' + missionId String + userId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + mission Mission @relation(...) + user User @relation(...) + + @@unique([missionId, userId, role]) // Un utilisateur ne peut avoir qu'un rôle par mission +} +``` + +**Exemple de données créées** : +```typescript +missionUsers = [ + { missionId: "abc-123", userId: "user-1", role: "gardien-temps" }, + { missionId: "abc-123", userId: "user-2", role: "gardien-parole" }, + { missionId: "abc-123", userId: "user-3", role: "gardien-memoire" }, + { missionId: "abc-123", userId: "user-4", role: "volontaire" }, + { missionId: "abc-123", userId: "user-5", role: "volontaire" } +] +``` + +**Points importants** : +- ✅ Utilisation de `createMany()` pour performance (1 requête au lieu de N) +- ✅ Contrainte unique : un utilisateur ne peut avoir qu'un rôle par mission +- ✅ Les gardiens sont obligatoires, les volontaires optionnels + +### 2.4 STEP 3 : Upload du Logo vers Minio + +**Lignes 285-310** + +```typescript +let logoPath = null; +if (body.logo?.data) { + try { + // 3.1 Conversion base64 → Buffer → File + const base64Data = body.logo.data.split(',')[1]; // Retire "data:image/png;base64," + const buffer = Buffer.from(base64Data, 'base64'); + const file = new File( + [buffer], + body.logo.name || 'logo.png', + { type: body.logo.type || 'image/png' } + ); + + // 3.2 Upload vers Minio + const { filePath } = await uploadMissionLogo(userId, mission.id, file); + logoPath = filePath; // Ex: "missions/abc-123/logo.png" + uploadedFiles.push({ type: 'logo', path: filePath }); + + // 3.3 Mise à jour de la mission avec le chemin du logo + await prisma.mission.update({ + where: { id: mission.id }, + data: { logo: filePath } + }); + } catch (uploadError) { + throw new Error('Failed to upload logo'); + } +} +``` + +**Fonction `uploadMissionLogo()` - `lib/mission-uploads.ts`** : + +**Lignes 96-145** + +```typescript +export async function uploadMissionLogo( + userId: string, + missionId: string, + file: File +): Promise<{ filePath: string }> { + + // 1. Génération du chemin + const fileExtension = file.name.substring(file.name.lastIndexOf('.')); + const filePath = getMissionLogoPath(userId, missionId, fileExtension); + // Résultat : "missions/{missionId}/logo.png" + + // 2. Conversion pour Minio (retire le préfixe "missions/") + const minioPath = filePath.replace(/^missions\//, ''); + // Résultat : "{missionId}/logo.png" + + // 3. Conversion File → Buffer + const arrayBuffer = await file.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + + // 4. Upload vers Minio via S3 SDK + await s3Client.send(new PutObjectCommand({ + Bucket: 'missions', // Bucket Minio + Key: minioPath, // "{missionId}/logo.png" + Body: buffer, // Contenu du fichier + ContentType: file.type, // "image/png" + ACL: 'public-read' // Accès public en lecture + })); + + return { filePath }; // Retourne le chemin complet avec préfixe +} +``` + +**Configuration Minio** : +```typescript +// lib/mission-uploads.ts +const s3Client = new S3Client({ + region: 'us-east-1', + endpoint: 'https://dome-api.slm-lab.net', // Endpoint Minio + credentials: { + accessKeyId: '4aBT4CMb7JIMMyUtp4Pl', + secretAccessKey: 'HGn39XhCIlqOjmDVzRK9MED2Fci2rYvDDgbLFElg' + }, + forcePathStyle: true // Requis pour MinIO +}); +``` + +**Structure dans Minio** : +``` +Bucket: missions + └── {missionId}/ + └── logo.png +``` + +**Points importants** : +- ✅ Le logo est uploadé **APRÈS** la création de la mission (pour avoir le missionId) +- ✅ Le chemin est mis à jour dans Prisma après l'upload +- ✅ Le fichier est stocké avec ACL `public-read` pour accès public +- ✅ Le chemin stocké inclut le préfixe `missions/` pour cohérence + +### 2.5 STEP 4 : Upload des Attachments vers Minio + +**Lignes 312-343** + +```typescript +if (body.attachments && body.attachments.length > 0) { + try { + // 4.1 Traitement parallèle de tous les attachments + const attachmentPromises = body.attachments.map(async (attachment: any) => { + // Conversion base64 → Buffer → File + const base64Data = attachment.data.split(',')[1]; + const buffer = Buffer.from(base64Data, 'base64'); + const file = new File( + [buffer], + attachment.name || 'attachment', + { type: attachment.type || 'application/octet-stream' } + ); + + // 4.2 Upload vers Minio + const { filePath, filename, fileType, fileSize } = + await uploadMissionAttachment(userId, mission.id, file); + uploadedFiles.push({ type: 'attachment', path: filePath }); + + // 4.3 Création de l'enregistrement Attachment en base + return prisma.attachment.create({ + data: { + missionId: mission.id, + filename, // "document.pdf" + filePath, // "missions/abc-123/attachments/document.pdf" + fileType, // "application/pdf" + fileSize, // 123456 (bytes) + uploaderId: userId + } + }); + }); + + // 4.4 Attente de tous les uploads (parallèle) + await Promise.all(attachmentPromises); + } catch (attachmentError) { + throw new Error('Failed to upload attachments'); + } +} +``` + +**Fonction `uploadMissionAttachment()` - `lib/mission-uploads.ts`** : + +**Lignes 148-210** + +```typescript +export async function uploadMissionAttachment( + userId: string, + missionId: string, + file: File +): Promise<{ + filename: string; + filePath: string; + fileType: string; + fileSize: number; +}> { + + // 1. Génération du chemin + const filePath = getMissionAttachmentPath(userId, missionId, file.name); + // Résultat : "missions/{missionId}/attachments/{filename}" + + // 2. Conversion pour Minio + const minioPath = filePath.replace(/^missions\//, ''); + // Résultat : "{missionId}/attachments/{filename}" + + // 3. Conversion File → Buffer + const arrayBuffer = await file.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + + // 4. Upload vers Minio + await s3Client.send(new PutObjectCommand({ + Bucket: 'missions', + Key: minioPath, + Body: buffer, + ContentType: file.type, + ACL: 'public-read' + })); + + return { + filename: file.name, + filePath, // Chemin complet avec préfixe + fileType: file.type, + fileSize: file.size + }; +} +``` + +**Schéma Prisma Attachment** : +```prisma +model Attachment { + id String @id @default(uuid()) + filename String // "document.pdf" + filePath String // "missions/{missionId}/attachments/{filename}" + fileType String // "application/pdf" + fileSize Int // 123456 + missionId String + uploaderId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + mission Mission @relation(...) + uploader User @relation(...) +} +``` + +**Structure dans Minio** : +``` +Bucket: missions + └── {missionId}/ + ├── logo.png + └── attachments/ + ├── document1.pdf + ├── document2.docx + └── image.jpg +``` + +**Points importants** : +- ✅ Uploads en **parallèle** avec `Promise.all()` pour performance +- ✅ Chaque attachment crée un enregistrement Prisma séparé +- ✅ Le `uploaderId` est l'utilisateur qui a créé la mission +- ✅ Les fichiers sont stockés avec ACL `public-read` + +### 2.6 STEP 5 : Vérification des Fichiers dans Minio + +**Lignes 345-365** + +```typescript +// 5.1 Vérification du logo +if (logoPath) { + const logoExists = await verifyFileExists(logoPath); + if (!logoExists) { + throw new Error('Logo file not found in Minio'); + } +} + +// 5.2 Vérification des attachments +if (body.attachments?.length > 0) { + const attachmentVerifications = uploadedFiles + .filter(f => f.type === 'attachment') + .map(f => verifyFileExists(f.path)); + + const attachmentResults = await Promise.all(attachmentVerifications); + if (attachmentResults.some(exists => !exists)) { + throw new Error('One or more attachment files not found in Minio'); + } +} +``` + +**Fonction `verifyFileExists()` - Lignes 191-202** + +```typescript +async function verifyFileExists(filePath: string): Promise { + try { + await s3Client.send(new HeadObjectCommand({ + Bucket: 'missions', + Key: filePath.replace('missions/', '') // Retire le préfixe + })); + return true; // Fichier existe + } catch (error) { + return false; // Fichier n'existe pas + } +} +``` + +**Points importants** : +- ✅ Vérification **AVANT** de déclencher N8N +- ✅ Utilise `HeadObjectCommand` (légère, ne télécharge pas le fichier) +- ✅ Si un fichier manque, le workflow s'arrête avec erreur + +### 2.7 STEP 6 : Déclenchement du Workflow N8N + +**Lignes 367-393** + +```typescript +// 6.1 Préparation des données pour N8N +const n8nData = { + ...body, // Toutes les données de la mission + creatorId: userId, // ID du créateur + logoPath: logoPath, // Chemin du logo (ou null) + config: { + N8N_API_KEY: process.env.N8N_API_KEY, + MISSION_API_URL: process.env.NEXT_PUBLIC_API_URL + } +}; + +// 6.2 Déclenchement du workflow +const n8nService = new N8nService(); +const workflowResult = await n8nService.triggerMissionCreation(n8nData); + +// 6.3 Vérification du résultat +if (!workflowResult.success) { + throw new Error(workflowResult.error || 'N8N workflow failed'); +} + +// 6.4 Retour succès +return NextResponse.json({ + success: true, + mission, + message: 'Mission created successfully with all integrations' +}); +``` + +--- + +## 🔗 ÉTAPE 3 : Service N8N + +### Fichier : `lib/services/n8n-service.ts` + +### 3.1 Configuration + +**Lignes 3-17** + +```typescript +export class N8nService { + private webhookUrl: string; + private rollbackWebhookUrl: string; + private apiKey: string; + + constructor() { + this.webhookUrl = process.env.N8N_WEBHOOK_URL || + 'https://brain.slm-lab.net/webhook/mission-created'; + this.rollbackWebhookUrl = process.env.N8N_ROLLBACK_WEBHOOK_URL || + 'https://brain.slm-lab.net/webhook/mission-rollback'; + this.apiKey = process.env.N8N_API_KEY || ''; + } +} +``` + +### 3.2 Nettoyage et Validation des Données + +**Lignes 19-49** + +```typescript +async triggerMissionCreation(data: any): Promise { + // Nettoyage des données + const cleanData = { + name: data.name, + oddScope: Array.isArray(data.oddScope) ? data.oddScope : [data.oddScope], + niveau: data.niveau || 'default', + intention: data.intention?.trim() || '', + missionType: data.missionType || 'default', + donneurDOrdre: data.donneurDOrdre || 'default', + projection: data.projection || 'default', + services: Array.isArray(data.services) ? data.services : [], + participation: data.participation || 'default', + profils: Array.isArray(data.profils) ? data.profils : [], + guardians: data.guardians || {}, + volunteers: Array.isArray(data.volunteers) ? data.volunteers : [], + creatorId: data.creatorId, + config: { + ...data.config, + N8N_API_KEY: this.apiKey, + MISSION_API_URL: process.env.NEXT_PUBLIC_API_URL || 'https://api.slm-lab.net/api' + } + }; +} +``` + +**Points importants** : +- ✅ Normalisation des arrays (assure qu'ils sont bien des arrays) +- ✅ Valeurs par défaut pour éviter les undefined +- ✅ Trim de l'intention pour retirer les espaces +- ✅ Conservation de la config avec les clés API + +### 3.3 Envoi au Webhook N8N + +**Lignes 73-96** + +```typescript +const response = await fetch(this.webhookUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': this.apiKey // Authentification + }, + body: JSON.stringify(cleanData) +}); + +if (!response.ok) { + const errorText = await response.text(); + throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`); +} +``` + +### 3.4 Traitement de la Réponse + +**Lignes 98-143** + +```typescript +const responseText = await response.text(); + +try { + const result = JSON.parse(responseText); + + // Détection d'erreurs partielles + if (result.error || result.message?.includes('failed')) { + const errorMessage = result.message || result.error; + const failedServices = { + gitRepo: errorMessage.includes('Git repository creation failed'), + leantimeProject: errorMessage.includes('Leantime project creation failed'), + docCollection: errorMessage.includes('Documentation collection creation failed'), + rocketChatChannel: errorMessage.includes('RocketChat channel creation failed') + }; + + // Retourne succès avec erreurs partielles + return { + success: true, + results: { ...result, failedServices } + }; + } + + return { success: true, results: result }; + +} catch (parseError) { + // Si la réponse n'est pas JSON, considère comme succès + return { + success: true, + results: { + logoUrl: null, + leantimeProjectId: null, + outlineCollectionId: null, + rocketChatChannelId: null, + giteaRepositoryUrl: null + } + }; +} +``` + +**Format de réponse attendu de N8N** : +```json +{ + "leantimeProjectId": "project-123", + "outlineCollectionId": "collection-456", + "rocketChatChannelId": "channel-789", + "giteaRepositoryUrl": "https://git.slm-lab.net/mission-abc", + "penpotProjectId": "penpot-xyz" +} +``` + +**Intégrations créées par N8N** : +1. **Leantime** : Projet de gestion de projet +2. **Outline** : Collection de documentation +3. **RocketChat** : Canal de communication +4. **Gitea** : Repository Git +5. **Penpot** : Projet de design + +**Points importants** : +- ✅ Gestion des erreurs partielles (certains services peuvent échouer) +- ✅ Si la réponse n'est pas JSON, considère comme succès (workflow déclenché) +- ✅ Les IDs retournés ne sont **PAS** sauvegardés en base (TODO) + +--- + +## 🧹 Gestion des Erreurs et Cleanup + +### Fichier : `app/api/missions/route.ts` - Lignes 398-418 + +```typescript +catch (error) { + console.error('Error in mission creation:', error); + + // Cleanup: Suppression de tous les fichiers uploadés + for (const file of uploadedFiles) { + try { + await s3Client.send(new DeleteObjectCommand({ + Bucket: 'missions', + Key: file.path.replace('missions/', '') + })); + console.log('Cleaned up file:', file.path); + } catch (cleanupError) { + console.error('Error cleaning up file:', file.path, cleanupError); + } + } + + return NextResponse.json({ + error: 'Failed to create mission', + details: error instanceof Error ? error.message : String(error) + }, { status: 500 }); +} +``` + +**Scénarios de cleanup** : +1. ✅ Erreur lors de l'upload du logo → Suppression du logo +2. ✅ Erreur lors de l'upload d'un attachment → Suppression de tous les fichiers +3. ✅ Erreur lors de la vérification Minio → Suppression de tous les fichiers +4. ✅ Erreur N8N → Suppression de tous les fichiers + +**Points importants** : +- ✅ La mission reste en base même en cas d'erreur (orphan) +- ✅ Les MissionUsers restent en base même en cas d'erreur +- ✅ Seuls les fichiers Minio sont nettoyés +- ⚠️ **TODO** : Rollback complet (suppression mission + users si erreur) + +--- + +## 📊 Résumé des Opérations Prisma + +### Requêtes Prisma dans l'ordre d'exécution : + +1. **`prisma.mission.create()`** + - Crée la mission avec tous les champs + - Génère un UUID pour `id` + - `logo` = `null` initialement + +2. **`prisma.missionUser.createMany()`** + - Crée tous les gardiens et volontaires en une requête + - Utilise `createMany()` pour performance + +3. **`prisma.mission.update()`** (si logo) + - Met à jour le champ `logo` avec le chemin Minio + +4. **`prisma.attachment.create()`** (pour chaque attachment) + - Créé en parallèle avec `Promise.all()` + - Un enregistrement par fichier + +**Total** : 1 + 1 + (0 ou 1) + N requêtes Prisma +- Minimum : 2 requêtes (mission + users) +- Avec logo : 3 requêtes +- Avec N attachments : 3 + N requêtes + +--- + +## 📦 Résumé des Opérations Minio + +### Uploads Minio dans l'ordre d'exécution : + +1. **Logo** (si présent) + - Bucket : `missions` + - Key : `{missionId}/logo.png` + - Path stocké : `missions/{missionId}/logo.png` + +2. **Attachments** (si présents, en parallèle) + - Bucket : `missions` + - Key : `{missionId}/attachments/{filename}` + - Path stocké : `missions/{missionId}/attachments/{filename}` + +3. **Vérifications** (après uploads) + - `HeadObjectCommand` pour chaque fichier + - Vérifie l'existence avant N8N + +**Total** : 1 + N uploads + (1 + N) vérifications + +--- + +## 🔄 Résumé du Workflow N8N + +### Données envoyées à N8N : + +```typescript +{ + name: "Mission Example", + oddScope: ["odd-3"], + niveau: "a", + intention: "Description...", + missionType: "remote", + donneurDOrdre: "individual", + projection: "short", + services: ["Gite", "ArtLab"], + participation: "volontaire", + profils: ["DataIntelligence", "Expression"], + guardians: { + "gardien-temps": "user-1", + "gardien-parole": "user-2", + "gardien-memoire": "user-3" + }, + volunteers: ["user-4", "user-5"], + creatorId: "user-creator", + logoPath: "missions/abc-123/logo.png", + config: { + N8N_API_KEY: "...", + MISSION_API_URL: "https://api.slm-lab.net/api" + } +} +``` + +### Actions N8N (workflow interne) : + +1. Création projet Leantime +2. Création collection Outline +3. Création canal RocketChat +4. Création repository Gitea +5. Création projet Penpot + +**Note** : Les IDs retournés ne sont **PAS** sauvegardés en base actuellement. + +--- + +## ⚠️ Points d'Attention et TODOs + +### 1. Sauvegarde des IDs N8N +**Problème** : Les IDs retournés par N8N (leantimeProjectId, etc.) ne sont pas sauvegardés en base. + +**Solution proposée** : +```typescript +// Après le workflow N8N +if (workflowResult.success && workflowResult.results) { + await prisma.mission.update({ + where: { id: mission.id }, + data: { + leantimeProjectId: workflowResult.results.leantimeProjectId, + outlineCollectionId: workflowResult.results.outlineCollectionId, + rocketChatChannelId: workflowResult.results.rocketChatChannelId, + giteaRepositoryUrl: workflowResult.results.giteaRepositoryUrl, + penpotProjectId: workflowResult.results.penpotProjectId + } + }); +} +``` + +### 2. Rollback Complet +**Problème** : En cas d'erreur, la mission reste en base (orphan). + +**Solution proposée** : +```typescript +catch (error) { + // Suppression de la mission et des relations + await prisma.mission.delete({ where: { id: mission.id } }); + // Les MissionUsers et Attachments seront supprimés en cascade + // Nettoyage Minio... +} +``` + +### 3. Credentials Minio Hardcodés +**Problème** : Les credentials Minio sont hardcodés dans `lib/mission-uploads.ts`. + +**Solution** : Déplacer vers variables d'environnement. + +### 4. Gestion des Erreurs N8N Partielles +**Problème** : Si certains services N8N échouent, on continue quand même. + +**Solution** : Décider si on continue ou on rollback selon la criticité. + +--- + +## 📈 Performance et Optimisations + +### Optimisations actuelles : +- ✅ `createMany()` pour MissionUsers (1 requête au lieu de N) +- ✅ `Promise.all()` pour les attachments (parallèle) +- ✅ `HeadObjectCommand` pour vérification (léger) + +### Optimisations possibles : +- 🔄 Transaction Prisma pour atomicité +- 🔄 Batch upload Minio (si supporté) +- 🔄 Retry logic pour N8N +- 🔄 Cache des vérifications Minio + +--- + +## 🔍 Debugging et Logs + +### Points de logging importants : + +1. **Début du workflow** : `=== Mission Creation Started ===` +2. **Création Prisma** : `Mission created successfully` +3. **Upload Minio** : `Logo upload successful` / `Attachment upload successful` +4. **Vérification** : `verifyFileExists()` logs +5. **N8N** : `=== Starting N8N Workflow ===` / `N8N Workflow Result` +6. **Erreurs** : Tous les catch blocks loggent les erreurs + +### Commandes de debug : + +```bash +# Voir les logs du serveur +npm run dev # Logs dans la console + +# Vérifier Minio +# Accéder à https://dome-api.slm-lab.net +# Bucket: missions + +# Vérifier Prisma +npx prisma studio # Interface graphique +``` + +--- + +**Document généré le** : $(date) +**Version** : 1.0 +**Auteur** : Analyse complète du codebase + diff --git a/MISSION_DELETION_WORKFLOW.md b/MISSION_DELETION_WORKFLOW.md new file mode 100644 index 00000000..14c88f35 --- /dev/null +++ b/MISSION_DELETION_WORKFLOW.md @@ -0,0 +1,625 @@ +# Workflow Détaillé : Suppression de Mission - Centrale (/missions) + +## 📋 Vue d'Ensemble + +Ce document trace **chaque étape** du workflow de suppression d'une mission depuis la page Centrale (`/missions/[missionId]`), incluant les vérifications de permissions, la suppression des fichiers Minio, et la suppression en base de données. + +--- + +## 🔄 Flux Complet - Vue d'Ensemble + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 1. FRONTEND - MissionDetailPage │ +│ - Clic sur bouton "Supprimer" │ +│ - Confirmation utilisateur (confirm dialog) │ +│ - DELETE /api/missions/[missionId] │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 2. BACKEND - DELETE /api/missions/[missionId] │ +│ ├─ Authentification (NextAuth) │ +│ ├─ Vérification mission existe │ +│ ├─ Vérification permissions (créateur ou admin) │ +│ ├─ STEP 1: Suppression logo Minio (si existe) │ +│ ├─ STEP 2: Rollback N8N (TODO - non implémenté) │ +│ └─ STEP 3: Suppression mission Prisma (CASCADE) │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 3. PRISMA CASCADE │ +│ - Suppression automatique MissionUsers │ +│ - Suppression automatique Attachments │ +│ ⚠️ Fichiers Minio des attachments NON supprimés │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 📝 ÉTAPE 1 : Frontend - Clic sur Supprimer + +### Fichier : `app/missions/[missionId]/page.tsx` + +### 1.1 Bouton Supprimer + +**Lignes 397-411** + +```typescript + +``` + +**Caractéristiques** : +- ✅ Bouton rouge avec icône Trash2 +- ✅ Désactivé pendant la suppression (`deleting` state) +- ✅ Affiche un spinner pendant le traitement + +### 1.2 Handler de Suppression + +**Lignes 143-176** + +```typescript +const handleDeleteMission = async () => { + // 1. Confirmation utilisateur + if (!confirm("Êtes-vous sûr de vouloir supprimer cette mission ? Cette action est irréversible.")) { + return; // Annulation si l'utilisateur refuse + } + + try { + setDeleting(true); // Active le spinner + + // 2. Appel API DELETE + const response = await fetch(`/api/missions/${missionId}`, { + method: 'DELETE', + }); + + // 3. Vérification de la réponse + if (!response.ok) { + throw new Error('Failed to delete mission'); + } + + // 4. Notification de succès + toast({ + title: "Mission supprimée", + description: "La mission a été supprimée avec succès", + }); + + // 5. Redirection vers la liste des missions + router.push('/missions'); + + } catch (error) { + console.error('Error deleting mission:', error); + toast({ + title: "Erreur", + description: "Impossible de supprimer la mission", + variant: "destructive", + }); + } finally { + setDeleting(false); // Désactive le spinner + } +}; +``` + +**Points importants** : +- ✅ **Double confirmation** : Dialog natif du navigateur +- ✅ **Gestion d'état** : `deleting` pour le spinner +- ✅ **Redirection automatique** vers `/missions` en cas de succès +- ✅ **Gestion d'erreurs** avec toast notification + +--- + +## 🗄️ ÉTAPE 2 : Backend - DELETE /api/missions/[missionId] + +### Fichier : `app/api/missions/[missionId]/route.ts` + +### 2.1 Authentification et Récupération de la Mission + +**Lignes 292-316** + +```typescript +export async function DELETE( + request: Request, + props: { params: Promise<{ missionId: string }> } +) { + const params = await props.params; + + try { + // 1. Vérification de l'authentification + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // 2. Récupération de la mission avec relations + const mission = await prisma.mission.findUnique({ + where: { id: params.missionId }, + include: { + missionUsers: { + include: { + user: true // Inclut les infos des utilisateurs + } + } + } + }); + + // 3. Vérification que la mission existe + if (!mission) { + return NextResponse.json({ error: 'Mission not found' }, { status: 404 }); + } +``` + +**Points importants** : +- ✅ Vérification de session NextAuth +- ✅ Récupération de la mission avec `missionUsers` (pour logging/info) +- ✅ Retour 404 si mission n'existe pas + +### 2.2 Vérification des Permissions + +**Lignes 318-324** + +```typescript +// Vérification : utilisateur doit être créateur OU admin +const isCreator = mission.creatorId === session.user.id; +const userRoles = Array.isArray(session.user.role) ? session.user.role : []; +const isAdmin = userRoles.includes('admin') || userRoles.includes('ADMIN'); + +if (!isCreator && !isAdmin) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); +} +``` + +**Règles de permissions** : +- ✅ **Créateur** : Peut supprimer sa propre mission +- ✅ **Admin** : Peut supprimer n'importe quelle mission +- ❌ **Autres utilisateurs** : Même gardiens/volontaires ne peuvent pas supprimer + +**Points importants** : +- ✅ Vérification stricte : seul le créateur ou un admin peut supprimer +- ✅ Les gardiens (même gardien-memoire) ne peuvent pas supprimer +- ✅ Retour 403 Forbidden si pas autorisé + +### 2.3 STEP 1 : Suppression du Logo dans Minio + +**Lignes 326-334** + +```typescript +// Suppression du logo si présent +if (mission.logo) { + try { + await deleteMissionLogo(params.missionId, mission.logo); + } catch (error) { + console.error('Error deleting mission logo:', error); + // Continue deletion even if logo deletion fails + } +} +``` + +**Fonction `deleteMissionLogo()` - `lib/mission-uploads.ts`** : + +**Lignes 42-61** + +```typescript +export async function deleteMissionLogo( + missionId: string, + logoPath: string +): Promise { + try { + const normalizedPath = ensureMissionsPrefix(logoPath); + // ⚠️ TODO: La fonction ne fait que logger, ne supprime pas vraiment ! + console.log('Deleting mission logo:', { + missionId, + originalPath: logoPath, + normalizedPath + }); + // TODO: Implémenter la suppression réelle avec DeleteObjectCommand + } catch (error) { + console.error('Error deleting mission logo:', error); + throw error; + } +} +``` + +**⚠️ PROBLÈME IDENTIFIÉ** : +- ❌ La fonction `deleteMissionLogo()` ne supprime **PAS** réellement le fichier +- ❌ Elle ne fait que logger les informations +- ⚠️ Le logo reste dans Minio après suppression de la mission + +**Solution proposée** : +```typescript +export async function deleteMissionLogo( + missionId: string, + logoPath: string +): Promise { + try { + const { DeleteObjectCommand } = await import('@aws-sdk/client-s3'); + const normalizedPath = ensureMissionsPrefix(logoPath); + const minioPath = normalizedPath.replace(/^missions\//, ''); + + await s3Client.send(new DeleteObjectCommand({ + Bucket: 'missions', + Key: minioPath + })); + + console.log('Mission logo deleted successfully:', minioPath); + } catch (error) { + console.error('Error deleting mission logo:', error); + throw error; + } +} +``` + +**Points importants** : +- ✅ Continue la suppression même si le logo échoue (try/catch) +- ⚠️ **BUG** : Le logo n'est pas réellement supprimé actuellement + +### 2.4 STEP 2 : Rollback N8N (TODO - Non Implémenté) + +**Lignes 336-344** + +```typescript +// Trigger n8n workflow for rollback +// TODO: Implement rollbackMission method in N8nService +// const n8nService = new N8nService(); +// try { +// await n8nService.rollbackMission(mission); +// } catch (error) { +// console.error('Error during mission rollback:', error); +// // Continue with mission deletion even if rollback fails +// } +``` + +**⚠️ NON IMPLÉMENTÉ** : +- ❌ Le rollback N8N n'est pas appelé +- ❌ Les intégrations externes (Leantime, Outline, RocketChat, etc.) ne sont **PAS** supprimées +- ⚠️ Les ressources externes restent orphelines + +**Méthode disponible mais non utilisée** : +- ✅ `N8nService.triggerMissionRollback()` existe dans `lib/services/n8n-service.ts` +- ✅ Webhook URL : `https://brain.slm-lab.net/webhook/mission-rollback` +- ❌ Mais n'est pas appelée dans le DELETE + +**Solution proposée** : +```typescript +// Rollback N8N +const n8nService = new N8nService(); +try { + await n8nService.triggerMissionRollback({ + missionId: mission.id, + leantimeProjectId: mission.leantimeProjectId, + outlineCollectionId: mission.outlineCollectionId, + rocketChatChannelId: mission.rocketChatChannelId, + giteaRepositoryUrl: mission.giteaRepositoryUrl, + penpotProjectId: mission.penpotProjectId + }); +} catch (error) { + console.error('Error during mission rollback:', error); + // Continue with mission deletion even if rollback fails +} +``` + +### 2.5 STEP 3 : Suppression de la Mission en Base de Données + +**Lignes 346-349** + +```typescript +// Suppression de la mission (CASCADE automatique) +await prisma.mission.delete({ + where: { id: params.missionId } +}); +``` + +**Schéma Prisma - Relations avec CASCADE** : + +```prisma +model Mission { + // ... + attachments Attachment[] + missionUsers MissionUser[] +} + +model Attachment { + mission Mission @relation(fields: [missionId], references: [id], onDelete: Cascade) + // ... +} + +model MissionUser { + mission Mission @relation(fields: [missionId], references: [id], onDelete: Cascade) + // ... +} +``` + +**CASCADE automatique** : +- ✅ **MissionUsers** : Supprimés automatiquement (`onDelete: Cascade`) +- ✅ **Attachments** : Supprimés automatiquement (`onDelete: Cascade`) +- ❌ **Fichiers Minio** : **NON supprimés automatiquement** (pas de trigger) + +**Points importants** : +- ✅ Une seule requête Prisma supprime la mission et toutes ses relations +- ✅ Atomicité : Si la suppression échoue, rien n'est supprimé +- ⚠️ **PROBLÈME** : Les fichiers Minio des attachments ne sont pas supprimés + +### 2.6 Retour de Succès + +**Lignes 351-358** + +```typescript +return NextResponse.json({ success: true }); + +} catch (error) { + console.error('Error deleting mission:', error); + return NextResponse.json( + { error: 'Failed to delete mission' }, + { status: 500 } + ); +} +``` + +--- + +## 🔄 ÉTAPE 3 : Cascade Prisma + +### Suppression Automatique des Relations + +Quand `prisma.mission.delete()` est exécuté, Prisma supprime automatiquement : + +1. **Tous les MissionUsers** associés + ```sql + DELETE FROM "MissionUser" WHERE "missionId" = 'abc-123'; + ``` + +2. **Tous les Attachments** associés + ```sql + DELETE FROM "Attachment" WHERE "missionId" = 'abc-123'; + ``` + +**⚠️ PROBLÈME MAJEUR** : +- ❌ Les fichiers Minio des attachments ne sont **PAS** supprimés +- ❌ Les fichiers restent dans Minio : `missions/{missionId}/attachments/*` +- ⚠️ **Orphelins** : Fichiers sans enregistrement en base + +--- + +## 🧹 Problèmes Identifiés et Solutions + +### Problème 1 : Logo non supprimé dans Minio + +**Symptôme** : Le logo reste dans Minio après suppression + +**Cause** : `deleteMissionLogo()` ne fait que logger, ne supprime pas + +**Solution** : +```typescript +export async function deleteMissionLogo( + missionId: string, + logoPath: string +): Promise { + const { DeleteObjectCommand } = await import('@aws-sdk/client-s3'); + const normalizedPath = ensureMissionsPrefix(logoPath); + const minioPath = normalizedPath.replace(/^missions\//, ''); + + await s3Client.send(new DeleteObjectCommand({ + Bucket: 'missions', + Key: minioPath + })); +} +``` + +### Problème 2 : Attachments non supprimés dans Minio + +**Symptôme** : Les fichiers attachments restent dans Minio + +**Cause** : Pas de suppression des fichiers avant la suppression Prisma + +**Solution** : +```typescript +// Avant la suppression Prisma +if (mission.attachments && mission.attachments.length > 0) { + // Récupérer les attachments + const attachments = await prisma.attachment.findMany({ + where: { missionId: params.missionId } + }); + + // Supprimer chaque fichier Minio + for (const attachment of attachments) { + try { + await deleteMissionAttachment(attachment.filePath); + } catch (error) { + console.error('Error deleting attachment file:', error); + // Continue même si un fichier échoue + } + } +} +``` + +### Problème 3 : Rollback N8N non implémenté + +**Symptôme** : Les intégrations externes restent orphelines + +**Cause** : Code commenté, non implémenté + +**Solution** : +```typescript +// Décommenter et implémenter +const n8nService = new N8nService(); +try { + await n8nService.triggerMissionRollback({ + missionId: mission.id, + leantimeProjectId: mission.leantimeProjectId, + outlineCollectionId: mission.outlineCollectionId, + rocketChatChannelId: mission.rocketChatChannelId, + giteaRepositoryUrl: mission.giteaRepositoryUrl, + penpotProjectId: mission.penpotProjectId + }); +} catch (error) { + console.error('Error during mission rollback:', error); + // Continue avec la suppression même si rollback échoue +} +``` + +--- + +## 📊 Résumé des Opérations + +### Opérations Effectuées ✅ + +1. ✅ **Vérification authentification** : Session NextAuth +2. ✅ **Vérification permissions** : Créateur ou Admin +3. ✅ **Suppression Prisma Mission** : Avec cascade automatique +4. ✅ **Suppression Prisma MissionUsers** : Cascade automatique +5. ✅ **Suppression Prisma Attachments** : Cascade automatique + +### Opérations NON Effectuées ❌ + +1. ❌ **Suppression logo Minio** : Fonction ne fait que logger +2. ❌ **Suppression attachments Minio** : Pas de code pour supprimer +3. ❌ **Rollback N8N** : Code commenté, non implémenté + +--- + +## 🔍 Workflow Complet Corrigé (Proposé) + +```typescript +export async function DELETE(...) { + // 1. Authentification + const session = await getServerSession(authOptions); + if (!session?.user) return 401; + + // 2. Récupération mission avec attachments + const mission = await prisma.mission.findUnique({ + where: { id: params.missionId }, + include: { + attachments: true, // Inclure les attachments + missionUsers: true + } + }); + + if (!mission) return 404; + + // 3. Vérification permissions + const isCreator = mission.creatorId === session.user.id; + const isAdmin = session.user.role?.includes('admin'); + if (!isCreator && !isAdmin) return 403; + + // 4. Suppression logo Minio + if (mission.logo) { + try { + await deleteMissionLogo(mission.id, mission.logo); // À implémenter + } catch (error) { + console.error('Error deleting logo:', error); + // Continue + } + } + + // 5. Suppression attachments Minio + if (mission.attachments && mission.attachments.length > 0) { + for (const attachment of mission.attachments) { + try { + await deleteMissionAttachment(attachment.filePath); + } catch (error) { + console.error('Error deleting attachment:', error); + // Continue + } + } + } + + // 6. Rollback N8N + const n8nService = new N8nService(); + try { + await n8nService.triggerMissionRollback({ + missionId: mission.id, + leantimeProjectId: mission.leantimeProjectId, + outlineCollectionId: mission.outlineCollectionId, + rocketChatChannelId: mission.rocketChatChannelId, + giteaRepositoryUrl: mission.giteaRepositoryUrl, + penpotProjectId: mission.penpotProjectId + }); + } catch (error) { + console.error('Error during N8N rollback:', error); + // Continue même si rollback échoue + } + + // 7. Suppression Prisma (CASCADE) + await prisma.mission.delete({ + where: { id: params.missionId } + }); + + return NextResponse.json({ success: true }); +} +``` + +--- + +## 📈 Ordre d'Exécution Recommandé + +1. **Vérifications** (authentification, permissions, existence) +2. **Suppression fichiers Minio** (logo + attachments) +3. **Rollback N8N** (intégrations externes) +4. **Suppression Prisma** (mission + cascade automatique) + +**Pourquoi cet ordre ?** +- ✅ Supprimer les fichiers avant la base pour éviter les orphelins +- ✅ Rollback N8N avant suppression Prisma pour avoir les IDs +- ✅ Suppression Prisma en dernier (point de non-retour) + +--- + +## ⚠️ Points d'Attention + +### 1. Atomicité +**Problème** : Si une étape échoue, les précédentes sont déjà faites + +**Solution** : Transaction Prisma + Rollback manuel si erreur + +### 2. Performance +**Problème** : Suppression séquentielle des fichiers Minio + +**Solution** : `Promise.all()` pour suppressions parallèles + +### 3. Gestion d'Erreurs +**Problème** : Continue même si certaines suppressions échouent + +**Solution** : Décider si on continue ou on rollback selon criticité + +--- + +## 🔍 Debugging + +### Logs à surveiller : + +1. **Début suppression** : `DELETE /api/missions/[id]` +2. **Permissions** : `isCreator` / `isAdmin` +3. **Suppression logo** : `Deleting mission logo` +4. **Suppression attachments** : `Deleting mission attachment` +5. **Rollback N8N** : `Triggering n8n rollback workflow` +6. **Suppression Prisma** : `prisma.mission.delete()` + +### Vérifications manuelles : + +```bash +# Vérifier Minio +# Accéder à https://dome-api.slm-lab.net +# Bucket: missions +# Vérifier que les dossiers {missionId} sont supprimés + +# Vérifier Prisma +npx prisma studio +# Vérifier que la mission et ses relations sont supprimées +``` + +--- + +**Document généré le** : $(date) +**Version** : 1.0 +**Auteur** : Analyse complète du codebase + diff --git a/app/api/missions/[missionId]/route.ts b/app/api/missions/[missionId]/route.ts index 979c9876..71a79815 100644 --- a/app/api/missions/[missionId]/route.ts +++ b/app/api/missions/[missionId]/route.ts @@ -2,7 +2,7 @@ import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from "@/app/api/auth/options"; import { prisma } from '@/lib/prisma'; -import { deleteMissionLogo, getMissionFileUrl } from '@/lib/mission-uploads'; +import { deleteMissionLogo, deleteMissionAttachment, getMissionFileUrl } from '@/lib/mission-uploads'; import { getPublicUrl, S3_CONFIG } from '@/lib/s3'; import { N8nService } from '@/lib/services/n8n-service'; @@ -323,31 +323,72 @@ export async function DELETE( return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } + // Get attachments before deletion (needed for Minio cleanup) + const attachments = await prisma.attachment.findMany({ + where: { missionId: params.missionId } + }); + + // Step 1: Trigger N8N workflow for deletion (rollback external integrations) + console.log('=== Starting N8N Deletion Workflow ==='); + const n8nService = new N8nService(); + + const n8nDeletionData = { + missionId: mission.id, + name: mission.name, + leantimeProjectId: mission.leantimeProjectId, + outlineCollectionId: mission.outlineCollectionId, + rocketChatChannelId: mission.rocketChatChannelId, + giteaRepositoryUrl: mission.giteaRepositoryUrl, + penpotProjectId: mission.penpotProjectId, + config: { + N8N_API_KEY: process.env.N8N_API_KEY, + MISSION_API_URL: process.env.NEXT_PUBLIC_API_URL + } + }; + + console.log('Sending deletion data to N8N:', JSON.stringify(n8nDeletionData, null, 2)); + + const n8nResult = await n8nService.triggerMissionDeletion(n8nDeletionData); + console.log('N8N Deletion Workflow Result:', JSON.stringify(n8nResult, null, 2)); + + if (!n8nResult.success) { + console.error('N8N deletion workflow failed, but continuing with mission deletion:', n8nResult.error); + // Continue with deletion even if N8N fails (non-blocking) + } + + // Step 2: Delete files from Minio AFTER N8N confirmation // Delete logo if exists if (mission.logo) { try { await deleteMissionLogo(params.missionId, mission.logo); + console.log('Logo deleted successfully from Minio'); } catch (error) { - console.error('Error deleting mission logo:', error); + console.error('Error deleting mission logo from Minio:', error); // Continue deletion even if logo deletion fails } } - // Trigger n8n workflow for rollback - // TODO: Implement rollbackMission method in N8nService - // const n8nService = new N8nService(); - // try { - // await n8nService.rollbackMission(mission); - // } catch (error) { - // console.error('Error during mission rollback:', error); - // // Continue with mission deletion even if rollback fails - // } + // Delete attachments from Minio + if (attachments.length > 0) { + console.log(`Deleting ${attachments.length} attachment(s) from Minio...`); + for (const attachment of attachments) { + try { + await deleteMissionAttachment(attachment.filePath); + console.log(`Attachment deleted successfully: ${attachment.filename}`); + } catch (error) { + console.error(`Error deleting attachment ${attachment.filename} from Minio:`, error); + // Continue deletion even if one attachment fails + } + } + } - // Delete the mission + // Step 3: Delete the mission from database (CASCADE will delete MissionUsers and Attachments) await prisma.mission.delete({ where: { id: params.missionId } }); + console.log('Mission deleted successfully from database'); + return NextResponse.json({ success: true }); } catch (error) { console.error('Error deleting mission:', error); diff --git a/lib/mission-uploads.ts b/lib/mission-uploads.ts index f45a96b1..882d18ec 100644 --- a/lib/mission-uploads.ts +++ b/lib/mission-uploads.ts @@ -41,13 +41,25 @@ export function getMissionFileUrl(path: string): string { // Helper function to delete a mission logo export async function deleteMissionLogo(missionId: string, logoPath: string): Promise { try { + const { DeleteObjectCommand } = await import('@aws-sdk/client-s3'); const normalizedPath = ensureMissionsPrefix(logoPath); - // Add your S3/MinIO deletion logic here + const minioPath = normalizedPath.replace(/^missions\//, ''); // Remove prefix for Minio + console.log('Deleting mission logo:', { missionId, originalPath: logoPath, - normalizedPath + normalizedPath, + minioPath }); + + const command = new DeleteObjectCommand({ + Bucket: 'missions', + Key: minioPath, + }); + + await s3Client.send(command); + + console.log('Mission logo deleted successfully:', minioPath); } catch (error) { console.error('Error deleting mission logo:', { error, diff --git a/lib/services/n8n-service.ts b/lib/services/n8n-service.ts index d2944fbb..ac2ec552 100644 --- a/lib/services/n8n-service.ts +++ b/lib/services/n8n-service.ts @@ -16,6 +16,75 @@ export class N8nService { } } + async triggerMissionDeletion(data: any): Promise { + try { + const deleteWebhookUrl = process.env.N8N_DELETE_WEBHOOK_URL || 'https://brain.slm-lab.net/webhook-test/mission-delete'; + + console.log('Triggering n8n mission deletion workflow with data:', JSON.stringify(data, null, 2)); + console.log('Using deletion webhook URL:', deleteWebhookUrl); + console.log('API key present:', !!this.apiKey); + + const response = await fetch(deleteWebhookUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': this.apiKey + }, + body: JSON.stringify(data), + }); + + console.log('Deletion webhook response status:', response.status); + console.log('Deletion webhook response headers:', Object.fromEntries(response.headers.entries())); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Deletion webhook error response:', errorText); + // Try to parse the error response as JSON for more details + try { + const errorJson = JSON.parse(errorText); + console.error('Parsed error response:', errorJson); + } catch (e) { + console.error('Error response is not JSON'); + } + throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`); + } + + const responseText = await response.text(); + console.log('N8nService - Deletion raw response:', responseText); + + // Try to parse the response as JSON + try { + const result = JSON.parse(responseText); + console.log('Parsed deletion workflow result:', JSON.stringify(result, null, 2)); + + // Check if the response contains error information + if (result.error || result.message?.includes('failed')) { + return { + success: false, + error: result.message || result.error + }; + } + + return { + success: true, + results: result + }; + } catch (parseError) { + console.log('Response is not JSON, treating as workflow trigger confirmation'); + return { + success: true, + results: { confirmed: true } + }; + } + } catch (error) { + console.error('Error triggering n8n deletion workflow:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error' + }; + } + } + async triggerMissionCreation(data: any): Promise { try { console.log('N8nService - Input data:', {