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 } from '@/lib/mission-uploads'; // 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 => { console.log('Processing mission logo:', { missionId: mission.id, logo: 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) { console.error('Error listing missions:', 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) { console.error('Error verifying file:', filePath, error); return false; } } // POST endpoint to create a new mission export async function POST(request: Request) { let uploadedFiles: { type: 'logo' | 'attachment', path: string }[] = []; try { console.log('=== Mission Creation Started ==='); const { authorized, userId } = await checkAuth(request); if (!authorized || !userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const body = await request.json(); console.log('Received request body:', JSON.stringify(body, null, 2)); // 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 }; console.log('Creating mission with data:', JSON.stringify(missionData, null, 2)); const mission = await prisma.mission.create({ data: missionData }); console.log('Mission created successfully:', JSON.stringify(mission, null, 2)); // 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 }); console.log('Mission users created:', missionUsers); } // Step 3: Upload logo to Minio if present let logoPath = null; 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 }); // Update mission with logo path await prisma.mission.update({ where: { id: mission.id }, data: { logo: filePath } }); console.log('Logo uploaded successfully:', { logoPath }); } catch (uploadError) { console.error('Error uploading logo:', uploadError); 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); console.log('Attachments uploaded successfully'); } catch (attachmentError) { console.error('Error uploading attachments:', attachmentError); 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 console.log('=== Starting N8N Workflow ==='); const n8nService = new N8nService(); const n8nData = { ...body, creatorId: userId, logoPath: logoPath, config: { N8N_API_KEY: process.env.N8N_API_KEY, MISSION_API_URL: process.env.NEXT_PUBLIC_API_URL } }; console.log('Sending to N8N:', JSON.stringify(n8nData, null, 2)); const workflowResult = await n8nService.triggerMissionCreation(n8nData); console.log('N8N Workflow Result:', JSON.stringify(workflowResult, null, 2)); 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) { console.error('Error in final verification or n8n:', error); throw error; } } catch (error) { console.error('Error in mission creation:', error); // Cleanup: Delete any uploaded files 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 }); } }