18 KiB
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
<Button
variant="outline"
className="flex items-center gap-2 border-red-600 text-red-600 hover:bg-red-50 bg-white"
onClick={handleDeleteMission}
disabled={deleting}
>
{deleting ? (
<div className="animate-spin rounded-full h-4 w-4 border-t-2 border-b-2 border-red-600"></div>
) : (
<Trash2 className="h-4 w-4" />
)}
Supprimer
</Button>
Caractéristiques :
- ✅ Bouton rouge avec icône Trash2
- ✅ Désactivé pendant la suppression (
deletingstate) - ✅ Affiche un spinner pendant le traitement
1.2 Handler de Suppression
Lignes 143-176
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 :
deletingpour le spinner - ✅ Redirection automatique vers
/missionsen 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
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
// 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
// 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
export async function deleteMissionLogo(
missionId: string,
logoPath: string
): Promise<void> {
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 :
export async function deleteMissionLogo(
missionId: string,
logoPath: string
): Promise<void> {
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
// 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 danslib/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 :
// 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
// Suppression de la mission (CASCADE automatique)
await prisma.mission.delete({
where: { id: params.missionId }
});
Schéma Prisma - Relations avec CASCADE :
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
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 :
-
Tous les MissionUsers associés
DELETE FROM "MissionUser" WHERE "missionId" = 'abc-123'; -
Tous les Attachments associés
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 :
export async function deleteMissionLogo(
missionId: string,
logoPath: string
): Promise<void> {
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 :
// 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 :
// 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 ✅
- ✅ Vérification authentification : Session NextAuth
- ✅ Vérification permissions : Créateur ou Admin
- ✅ Suppression Prisma Mission : Avec cascade automatique
- ✅ Suppression Prisma MissionUsers : Cascade automatique
- ✅ Suppression Prisma Attachments : Cascade automatique
Opérations NON Effectuées ❌
- ❌ Suppression logo Minio : Fonction ne fait que logger
- ❌ Suppression attachments Minio : Pas de code pour supprimer
- ❌ Rollback N8N : Code commenté, non implémenté
🔍 Workflow Complet Corrigé (Proposé)
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é
- Vérifications (authentification, permissions, existence)
- Suppression fichiers Minio (logo + attachments)
- Rollback N8N (intégrations externes)
- 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 :
- Début suppression :
DELETE /api/missions/[id] - Permissions :
isCreator/isAdmin - Suppression logo :
Deleting mission logo - Suppression attachments :
Deleting mission attachment - Rollback N8N :
Triggering n8n rollback workflow - Suppression Prisma :
prisma.mission.delete()
Vérifications manuelles :
# 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