import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from "@/app/api/auth/options"; import { prisma } from '@/lib/prisma'; import { deleteMissionLogo, deleteMissionAttachment, getMissionFileUrl } from '@/lib/mission-uploads'; import { getPublicUrl, S3_CONFIG } from '@/lib/s3'; import { N8nService } from '@/lib/services/n8n-service'; // Helper function to check authentication async function checkAuth(request: Request) { const session = await getServerSession(authOptions); if (!session?.user?.id) { console.error('Unauthorized access attempt:', { url: request.url, method: request.method, headers: Object.fromEntries(request.headers) }); return { authorized: false, userId: null }; } return { authorized: true, userId: session.user.id }; } // GET endpoint to retrieve a mission by ID export async function GET(request: Request, props: { params: Promise<{ missionId: string }> }) { const params = await props.params; try { const { authorized, userId } = await checkAuth(request); if (!authorized || !userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { missionId } = params; if (!missionId) { return NextResponse.json({ error: 'Mission ID is required' }, { status: 400 }); } // Get mission with detailed info const mission = await (prisma as any).mission.findFirst({ where: { id: missionId, OR: [ { creatorId: userId }, { missionUsers: { some: { userId } } } ] }, include: { creator: { select: { id: true, email: true } }, missionUsers: { select: { id: true, role: true, user: { select: { id: true, email: true } } } }, attachments: { select: { id: true, filename: true, filePath: true, fileType: true, fileSize: true, createdAt: true }, orderBy: { createdAt: 'desc' } } } }); if (!mission) { return NextResponse.json({ error: 'Mission not found or access denied' }, { status: 404 }); } // Add public URLs to mission logo and attachments const missionWithUrls = { ...mission, logoUrl: mission.logo ? getMissionFileUrl(mission.logo) : null, logo: mission.logo, attachments: mission.attachments.map((attachment: { id: string; filename: string; filePath: string; fileType: string; fileSize: number; createdAt: Date }) => ({ ...attachment, publicUrl: getMissionFileUrl(attachment.filePath) })) }; console.log('Mission data with URLs:', { missionId: mission.id, logoPath: mission.logo, logoUrl: missionWithUrls.logoUrl, attachmentCount: mission.attachments.length }); return NextResponse.json(missionWithUrls); } catch (error) { console.error('Error retrieving mission:', { error, missionId: params.missionId, errorType: error instanceof Error ? error.constructor.name : typeof error, message: error instanceof Error ? error.message : String(error) }); return NextResponse.json({ error: 'Internal server error', details: error instanceof Error ? error.message : String(error) }, { status: 500 }); } } // PUT endpoint to update a mission export async function PUT(request: Request, props: { params: Promise<{ missionId: string }> }) { const params = await props.params; try { const { authorized, userId } = await checkAuth(request); if (!authorized || !userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { missionId } = params; if (!missionId) { return NextResponse.json({ error: 'Mission ID is required' }, { status: 400 }); } // Check if mission exists and user has access to modify it const existingMission = await (prisma as any).mission.findFirst({ where: { id: missionId, OR: [ { creatorId: userId }, { missionUsers: { some: { userId, role: { in: ['gardien-temps', 'gardien-parole'] } } } } ] } }); if (!existingMission) { return NextResponse.json({ error: 'Mission not found or not authorized to update' }, { status: 404 }); } // Parse the request body const body = await request.json(); const { name, logo, oddScope, niveau, intention, missionType, donneurDOrdre, projection, services, participation, profils, guardians, volunteers } = body; // Process logo URL to get relative path let logoPath = logo; if (logo && typeof logo === 'string') { try { // If it's a full URL, extract the path if (logo.startsWith('http')) { const url = new URL(logo); logoPath = url.pathname.startsWith('/') ? url.pathname.substring(1) : url.pathname; } // If it's already a relative path, ensure it starts with 'missions/' else if (!logo.startsWith('missions/')) { logoPath = `missions/${logo}`; } } catch (error) { console.error('Error processing logo URL:', { error, logo, missionId }); } } // Update the mission data const updatedMission = await (prisma as any).mission.update({ where: { id: missionId }, data: { name, logo: logoPath, oddScope: oddScope || undefined, niveau, intention, missionType, donneurDOrdre, projection, services: services || undefined, participation, profils: profils || undefined } }); // Update guardians if provided if (guardians) { // Get current guardians const currentGuardians = await (prisma as any).missionUser.findMany({ where: { missionId, role: { in: ['gardien-temps', 'gardien-parole', 'gardien-memoire'] } } }); // Delete all guardians if (currentGuardians.length > 0) { await (prisma as any).missionUser.deleteMany({ where: { missionId, role: { in: ['gardien-temps', 'gardien-parole', 'gardien-memoire'] } } }); } // Add new guardians const guardianRoles = ['gardien-temps', 'gardien-parole', 'gardien-memoire']; const guardianEntries = Object.entries(guardians) .filter(([role, userId]) => guardianRoles.includes(role) && userId) .map(([role, userId]) => ({ role, userId: userId as string, missionId })); if (guardianEntries.length > 0) { await (prisma as any).missionUser.createMany({ data: guardianEntries }); } } // Update volunteers if provided if (volunteers && Array.isArray(volunteers)) { // Get current volunteers const currentVolunteers = await (prisma as any).missionUser.findMany({ where: { missionId, role: 'volontaire' } }); // Delete all volunteers if (currentVolunteers.length > 0) { await (prisma as any).missionUser.deleteMany({ where: { missionId, role: 'volontaire' } }); } // Add new volunteers if (volunteers.length > 0) { const volunteerEntries = volunteers.map((userId: string) => ({ role: 'volontaire', userId, missionId })); await (prisma as any).missionUser.createMany({ data: volunteerEntries }); } } return NextResponse.json({ success: true, mission: { id: updatedMission.id, name: updatedMission.name, updatedAt: updatedMission.updatedAt } }); } catch (error) { console.error('Error updating mission:', error); return NextResponse.json({ error: 'Internal server error', details: error instanceof Error ? error.message : String(error) }, { status: 500 }); } } // DELETE endpoint to remove a mission export async function DELETE( request: Request, props: { params: Promise<{ missionId: string }> } ) { const params = await props.params; try { const session = await getServerSession(authOptions); if (!session?.user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const mission = await prisma.mission.findUnique({ where: { id: params.missionId }, include: { missionUsers: { include: { user: true } } } }); if (!mission) { return NextResponse.json({ error: 'Mission not found' }, { status: 404 }); } // Check if user is mission creator or 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 }); } // Get attachments before deletion (needed for Minio cleanup) const attachments = await prisma.attachment.findMany({ where: { missionId: params.missionId } }); // Step 1: Trigger N8N workflow for deletion (rollback external integrations) console.log('=== Starting N8N Deletion Workflow ==='); const n8nService = new N8nService(); // Extract repo name from giteaRepositoryUrl if present // Format: https://gite.slm-lab.net/alma/repo-name or https://gite.slm-lab.net/api/v1/repos/alma/repo-name let repoName = ''; if (mission.giteaRepositoryUrl) { try { const url = new URL(mission.giteaRepositoryUrl); // Extract repo name from path (last segment) const pathParts = url.pathname.split('/').filter(Boolean); repoName = pathParts[pathParts.length - 1] || ''; console.log('Extracted repo name from URL:', { url: mission.giteaRepositoryUrl, repoName }); } catch (error) { console.error('Error extracting repo name from URL:', error); // If URL parsing fails, try to extract from the string directly const match = mission.giteaRepositoryUrl.match(/\/([^\/]+)\/?$/); repoName = match ? match[1] : ''; } } // Prepare data according to N8N workflow expectations // The workflow expects: repoName, leantimeProjectId, documentationCollectionId, rocketchatChannelId const n8nDeletionData = { missionId: mission.id, name: mission.name, repoName: repoName, // N8N expects repoName, not giteaRepositoryUrl leantimeProjectId: mission.leantimeProjectId || 0, documentationCollectionId: mission.outlineCollectionId || '', // N8N expects documentationCollectionId rocketchatChannelId: mission.rocketChatChannelId || '', // N8N expects rocketchatChannelId (lowercase 'c') // Keep original fields for reference giteaRepositoryUrl: mission.giteaRepositoryUrl, outlineCollectionId: mission.outlineCollectionId, rocketChatChannelId: mission.rocketChatChannelId, penpotProjectId: mission.penpotProjectId, config: { N8N_API_KEY: process.env.N8N_API_KEY, MISSION_API_URL: process.env.NEXT_PUBLIC_API_URL || 'https://hub.slm-lab.net' } }; console.log('Sending deletion data to N8N:', JSON.stringify(n8nDeletionData, null, 2)); const n8nResult = await n8nService.triggerMissionDeletion(n8nDeletionData); console.log('N8N Deletion Workflow Result:', JSON.stringify(n8nResult, null, 2)); if (!n8nResult.success) { console.error('N8N deletion workflow failed, but continuing with mission deletion:', n8nResult.error); // Continue with deletion even if N8N fails (non-blocking) } // Step 2: Delete files from Minio AFTER N8N confirmation // Delete logo if exists if (mission.logo) { try { await deleteMissionLogo(params.missionId, mission.logo); console.log('Logo deleted successfully from Minio'); } catch (error) { console.error('Error deleting mission logo from Minio:', error); // Continue deletion even if logo deletion fails } } // Delete attachments from Minio if (attachments.length > 0) { console.log(`Deleting ${attachments.length} attachment(s) from Minio...`); for (const attachment of attachments) { try { await deleteMissionAttachment(attachment.filePath); console.log(`Attachment deleted successfully: ${attachment.filename}`); } catch (error) { console.error(`Error deleting attachment ${attachment.filename} from Minio:`, error); // Continue deletion even if one attachment fails } } } // Step 3: Delete the mission from database (CASCADE will delete MissionUsers and Attachments) await prisma.mission.delete({ where: { id: params.missionId } }); console.log('Mission deleted successfully from database'); return NextResponse.json({ success: true }); } catch (error) { console.error('Error deleting mission:', error); return NextResponse.json( { error: 'Failed to delete mission' }, { status: 500 } ); } }