import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from "@/app/api/auth/options"; import { prisma } from '@/lib/prisma'; import { N8nService } from '@/lib/services/n8n-service'; import { Prisma } from '@prisma/client'; import { s3Client } from '@/lib/s3'; import { CopyObjectCommand, DeleteObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3'; import { uploadMissionLogo, uploadMissionAttachment, getMissionFileUrl } from '@/lib/mission-uploads'; import { logger } from '@/lib/logger'; // Types interface MissionCreateInput { name: string; oddScope: string[]; niveau?: string; intention?: string; missionType?: string; donneurDOrdre?: string; projection?: string; services?: string[]; participation?: string; profils?: string[]; guardians?: Record; volunteers?: string[]; creatorId?: string; logo?: { data: string; name?: string; type?: string; } | null; attachments?: Array<{ data: string; name?: string; type?: string; }>; leantimeProjectId?: string | null; outlineCollectionId?: string | null; rocketChatChannelId?: string | null; giteaRepositoryUrl?: string | null; penpotProjectId?: string | null; } interface MissionUserInput { role: string; userId: string; missionId: string; } interface MissionResponse { id: string; name: string; oddScope: string[]; niveau: string; intention: string; missionType: string; donneurDOrdre: string; projection: string; services: string[]; profils: string[]; participation: string; creatorId: string; logo: string | null; leantimeProjectId: string | null; outlineCollectionId: string | null; rocketChatChannelId: string | null; giteaRepositoryUrl: string | null; penpotProjectId: string | null; createdAt: Date; updatedAt: Date; attachments?: Array<{ id: string; filename: string; filePath: string; fileType: string; fileSize: number; createdAt: Date; }>; } // Helper function to check authentication async function checkAuth(request: Request) { const session = await getServerSession(authOptions); return { authorized: !!session?.user, userId: session?.user?.id }; } // GET endpoint to list missions export async function GET(request: Request) { try { const { authorized, userId } = await checkAuth(request); if (!authorized || !userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { searchParams } = new URL(request.url); const limit = Number(searchParams.get('limit') || '10'); const offset = Number(searchParams.get('offset') || '0'); const search = searchParams.get('search'); const name = searchParams.get('name'); const where: Prisma.MissionWhereInput = {}; if (search) { where.OR = [ { name: { contains: search, mode: 'insensitive' } }, { intention: { contains: search, mode: 'insensitive' } } ]; } if (name) { where.name = name; } const missions = await prisma.mission.findMany({ where, skip: offset, take: limit, orderBy: { createdAt: 'desc' }, include: { creator: { select: { id: true, email: true } }, missionUsers: { include: { user: { select: { id: true, email: true } } } }, attachments: { select: { id: true, filename: true, filePath: true, fileType: true, fileSize: true, createdAt: true }, orderBy: { createdAt: 'desc' } } } }); const totalCount = await prisma.mission.count({ where }); // Transform missions to include public URLs const missionsWithUrls = missions.map(mission => { logger.debug('Processing mission logo:', { missionId: mission.id, hasLogo: !!mission.logo, constructedUrl: mission.logo ? `/api/missions/image/${mission.logo}` : null }); return { ...mission, logoUrl: mission.logo ? `/api/missions/image/${mission.logo}` : null, logo: mission.logo, attachments: mission.attachments?.map(attachment => ({ ...attachment, publicUrl: `/api/missions/image/${attachment.filePath}` })) || [] }; }); return NextResponse.json({ missions: missionsWithUrls, pagination: { total: totalCount, offset, limit } }); } catch (error) { logger.error('Error listing missions', { error: error instanceof Error ? error.message : String(error) }); return NextResponse.json({ error: 'Internal server error', details: error instanceof Error ? error.message : String(error) }, { status: 500 }); } } // Helper function to verify file exists in Minio async function verifyFileExists(filePath: string): Promise { try { await s3Client.send(new HeadObjectCommand({ Bucket: 'missions', Key: filePath.replace('missions/', '') })); return true; } catch (error) { logger.error('Error verifying file:', { filePath, error: error instanceof Error ? error.message : String(error) }); return false; } } // POST endpoint to create a new mission export async function POST(request: Request) { let uploadedFiles: { type: 'logo' | 'attachment', path: string }[] = []; try { logger.debug('Mission creation started'); const { authorized, userId } = await checkAuth(request); if (!authorized || !userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const body = await request.json(); logger.debug('Received mission creation request', { hasName: !!body.name, hasOddScope: !!body.oddScope, hasLogo: !!body.logo?.data, attachmentsCount: body.attachments?.length || 0 }); // Simple validation if (!body.name || !body.oddScope) { return NextResponse.json({ error: 'Missing required fields', missingFields: ['name', 'oddScope'].filter(field => !body[field]) }, { status: 400 }); } // Step 1: Create mission in database first const missionData = { name: body.name, oddScope: body.oddScope, niveau: body.niveau, intention: body.intention, missionType: body.missionType, donneurDOrdre: body.donneurDOrdre, projection: body.projection, services: body.services, profils: body.profils, participation: body.participation, creatorId: userId, logo: null, // Will update after upload }; logger.debug('Creating mission', { name: missionData.name, oddScope: missionData.oddScope, niveau: missionData.niveau, missionType: missionData.missionType }); const mission = await prisma.mission.create({ data: missionData }); logger.debug('Mission created successfully', { missionId: mission.id, name: mission.name }); // Step 2: Create mission users (guardians and volunteers) const missionUsers = []; // Add guardians if (body.guardians) { for (const [role, guardianId] of Object.entries(body.guardians)) { if (guardianId) { missionUsers.push({ missionId: mission.id, userId: guardianId, role: role }); } } } // Add volunteers if (body.volunteers && body.volunteers.length > 0) { for (const volunteerId of body.volunteers) { missionUsers.push({ missionId: mission.id, userId: volunteerId, role: 'volontaire' }); } } // Create all mission users if (missionUsers.length > 0) { await prisma.missionUser.createMany({ data: missionUsers }); logger.debug('Mission users created', { count: missionUsers.length, roles: missionUsers.map(u => u.role) }); } // Step 3: Upload logo to Minio if present let logoPath = null; let logoUrl = null; // Public URL for the logo if (body.logo?.data) { try { // Convert base64 to File object const base64Data = body.logo.data.split(',')[1]; const buffer = Buffer.from(base64Data, 'base64'); const file = new File([buffer], body.logo.name || 'logo.png', { type: body.logo.type || 'image/png' }); // Upload logo using the correct function const { filePath } = await uploadMissionLogo(userId, mission.id, file); logoPath = filePath; uploadedFiles.push({ type: 'logo', path: filePath }); // Generate public URL for the logo (using the API endpoint) // Use the helper function to construct the URL const relativeUrl = getMissionFileUrl(filePath); // Construct full URL with base domain const baseUrl = process.env.NEXT_PUBLIC_API_URL || process.env.NEXT_PUBLIC_APP_URL || 'https://hub.slm-lab.net'; logoUrl = `${baseUrl}${relativeUrl}`; // Update mission with logo path await prisma.mission.update({ where: { id: mission.id }, data: { logo: filePath } }); logger.debug('Logo uploaded successfully', { logoPath, hasLogoUrl: !!logoUrl }); } catch (uploadError) { logger.error('Error uploading logo', { error: uploadError instanceof Error ? uploadError.message : String(uploadError), missionId: mission.id }); throw new Error('Failed to upload logo'); } } // Step 4: Handle attachments if present if (body.attachments && body.attachments.length > 0) { try { const attachmentPromises = body.attachments.map(async (attachment: any) => { 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' }); // Upload attachment using the correct function const { filePath, filename, fileType, fileSize } = await uploadMissionAttachment(userId, mission.id, file); uploadedFiles.push({ type: 'attachment', path: filePath }); // Create attachment record in database with final path return prisma.attachment.create({ data: { missionId: mission.id, filename, filePath, fileType, fileSize, uploaderId: userId } }); }); await Promise.all(attachmentPromises); logger.debug('Attachments uploaded successfully', { count: body.attachments.length }); } catch (attachmentError) { logger.error('Error uploading attachments', { error: attachmentError instanceof Error ? attachmentError.message : String(attachmentError), missionId: mission.id }); throw new Error('Failed to upload attachments'); } } // Step 5: Verify all files are in Minio before triggering n8n try { // Verify logo if present if (logoPath) { const logoExists = await verifyFileExists(logoPath); if (!logoExists) { throw new Error('Logo file not found in Minio'); } } // Verify attachments if present 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'); } } // Only trigger n8n after verifying all files logger.debug('Starting N8N workflow'); const n8nService = new N8nService(); const n8nData = { ...body, missionId: mission.id, // ✅ Send missionId so N8N can return it in /mission-created creatorId: userId, logoPath: logoPath, logoUrl: logoUrl, // ✅ Send logo URL for N8N to use config: { N8N_API_KEY: process.env.N8N_API_KEY, MISSION_API_URL: process.env.NEXT_PUBLIC_API_URL } }; logger.debug('Sending to N8N', { missionId: n8nData.missionId, name: n8nData.name, hasLogo: !!n8nData.logoPath }); const workflowResult = await n8nService.triggerMissionCreation(n8nData); logger.debug('N8N workflow result', { success: workflowResult.success, hasError: !!workflowResult.error }); if (!workflowResult.success) { throw new Error(workflowResult.error || 'N8N workflow failed'); } return NextResponse.json({ success: true, mission, message: 'Mission created successfully with all integrations' }); } catch (error) { logger.error('Error in final verification or n8n', { error: error instanceof Error ? error.message : String(error) }); throw error; } } catch (error) { logger.error('Error in mission creation', { error: error instanceof Error ? error.message : String(error), uploadedFilesCount: uploadedFiles.length }); // Cleanup: Delete any uploaded files for (const file of uploadedFiles) { try { await s3Client.send(new DeleteObjectCommand({ Bucket: 'missions', Key: file.path.replace('missions/', '') })); logger.debug('Cleaned up file', { path: file.path }); } catch (cleanupError) { logger.error('Error cleaning up file', { path: file.path, error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError) }); } } return NextResponse.json({ error: 'Failed to create mission', details: error instanceof Error ? error.message : String(error) }, { status: 500 }); } }