983 lines
29 KiB
Markdown
983 lines
29 KiB
Markdown
# 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<boolean> {
|
|
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<any> {
|
|
// 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
|
|
|