NeahNew/MISSION_DELETION_WORKFLOW.md
2026-01-04 13:12:51 +01:00

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 (deleting state)
  • 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 : 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

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 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 :

// 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 :

  1. Tous les MissionUsers associés

    DELETE FROM "MissionUser" WHERE "missionId" = 'abc-123';
    
  2. 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

  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é)

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 :

# 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