626 lines
18 KiB
Markdown
626 lines
18 KiB
Markdown
# 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
|
|
<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**
|
|
|
|
```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<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** :
|
|
```typescript
|
|
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**
|
|
|
|
```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<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** :
|
|
```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
|
|
|