# 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