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 { deleteMissionLogo, deleteMissionAttachment } from '@/lib/mission-uploads'; import { S3Client, ListObjectsV2Command, DeleteObjectCommand } from '@aws-sdk/client-s3'; import { logger } from '@/lib/logger'; // S3 Configuration for missions - uses environment variables const MISSIONS_S3_CONFIG = { endpoint: (process.env.MINIO_S3_UPLOAD_BUCKET_URL || process.env.S3_ENDPOINT || 'https://dome-api.slm-lab.net').replace(/\/$/, ''), region: process.env.MINIO_AWS_REGION || process.env.S3_REGION || 'us-east-1', accessKey: process.env.MINIO_ACCESS_KEY || process.env.S3_ACCESS_KEY || '', secretKey: process.env.MINIO_SECRET_KEY || process.env.S3_SECRET_KEY || '', bucket: 'missions' }; const missionsS3Client = new S3Client({ region: MISSIONS_S3_CONFIG.region, endpoint: MISSIONS_S3_CONFIG.endpoint, credentials: { accessKeyId: MISSIONS_S3_CONFIG.accessKey, secretAccessKey: MISSIONS_S3_CONFIG.secretKey }, forcePathStyle: true }); // GET endpoint to get mission details including creator and missionUsers export async function GET( request: Request, { params }: { params: Promise<{ missionId: string }> } ) { try { const session = await getServerSession(authOptions); if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { missionId } = await params; const userId = session.user.id; // Find mission and check access const mission = await prisma.mission.findFirst({ where: { id: missionId, OR: [ { creatorId: userId }, { missionUsers: { some: { userId } } } ] }, select: { id: true, name: true, creatorId: true, isClosed: true, creator: { select: { id: true, email: true } }, missionUsers: { select: { id: true, role: true, userId: true, user: { select: { id: true, email: true } } } } } }); if (!mission) { return NextResponse.json({ error: 'Mission not found or access denied' }, { status: 404 }); } return NextResponse.json(mission); } catch (error: any) { console.error('Error fetching mission:', error); return NextResponse.json( { error: 'Failed to fetch mission', details: error.message }, { status: 500 } ); } } // DELETE endpoint to delete a mission export async function DELETE( request: Request, { params }: { params: Promise<{ missionId: string }> } ) { try { const session = await getServerSession(authOptions); if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { missionId } = await params; const userId = session.user.id; // Get mission with all related data const mission = await prisma.mission.findUnique({ where: { id: missionId }, include: { attachments: true, missionUsers: true, calendars: true } }); if (!mission) { return NextResponse.json({ error: 'Mission not found' }, { status: 404 }); } // Check if user has permission (only creator can delete) if (mission.creatorId !== userId) { return NextResponse.json({ error: 'Forbidden: Only the mission creator can delete the mission' }, { status: 403 }); } logger.debug('Starting mission deletion', { missionId, missionName: mission.name, hasLogo: !!mission.logo, attachmentsCount: mission.attachments.length }); // Step 1: Delete all files from S3/MinIO try { // Delete logo if exists if (mission.logo) { try { await deleteMissionLogo(missionId, mission.logo); logger.debug('Mission logo deleted from S3'); } catch (logoError) { logger.warn('Error deleting logo (continuing)', { error: logoError instanceof Error ? logoError.message : String(logoError) }); } } // Delete all attachments for (const attachment of mission.attachments) { try { await deleteMissionAttachment(attachment.filePath); logger.debug('Attachment deleted from S3', { filePath: attachment.filePath }); } catch (attachmentError) { logger.warn('Error deleting attachment (continuing)', { filePath: attachment.filePath, error: attachmentError instanceof Error ? attachmentError.message : String(attachmentError) }); } } // Delete all files in the mission folder (cleanup any orphaned files) try { const listCommand = new ListObjectsV2Command({ Bucket: MISSIONS_S3_CONFIG.bucket, Prefix: `${missionId}/` }); const listResponse = await missionsS3Client.send(listCommand); if (listResponse.Contents && listResponse.Contents.length > 0) { const deletePromises = listResponse.Contents.map(obj => { if (obj.Key) { return missionsS3Client.send(new DeleteObjectCommand({ Bucket: MISSIONS_S3_CONFIG.bucket, Key: obj.Key })); } }); await Promise.all(deletePromises); logger.debug('Cleaned up all files in mission folder', { count: listResponse.Contents.length }); } } catch (cleanupError) { logger.warn('Error cleaning up mission folder (continuing)', { error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError) }); } } catch (s3Error) { logger.error('Error deleting files from S3 (continuing with database deletion)', { error: s3Error instanceof Error ? s3Error.message : String(s3Error) }); } // Step 2: Extract repo name for N8N let repoName = ''; if (mission.giteaRepositoryUrl) { try { const url = new URL(mission.giteaRepositoryUrl); const pathParts = url.pathname.split('/').filter(Boolean); repoName = pathParts[pathParts.length - 1] || ''; } catch (error) { const match = mission.giteaRepositoryUrl.match(/\/([^\/]+)\/?$/); repoName = match ? match[1] : ''; } } // Step 3: Call N8N deletion webhook logger.debug('Preparing N8N deletion webhook call', { missionId: mission.id, hasGiteaUrl: !!mission.giteaRepositoryUrl, hasLeantimeId: !!mission.leantimeProjectId, hasOutlineId: !!mission.outlineCollectionId, hasRocketChatId: !!mission.rocketChatChannelId }); const n8nService = new N8nService(); const n8nData = { missionId: mission.id, name: mission.name, repoName: repoName, leantimeProjectId: mission.leantimeProjectId || 0, documentationCollectionId: mission.outlineCollectionId || '', rocketchatChannelId: mission.rocketChatChannelId || '', giteaRepositoryUrl: mission.giteaRepositoryUrl, outlineCollectionId: mission.outlineCollectionId, rocketChatChannelId: mission.rocketChatChannelId, penpotProjectId: mission.penpotProjectId }; logger.debug('Calling N8N deletion webhook', { webhookUrl: process.env.N8N_DELETE_WEBHOOK_URL || 'https://brain.slm-lab.net/webhook/mission-delete', hasApiKey: !!process.env.N8N_API_KEY, dataKeys: Object.keys(n8nData) }); // Call N8N but don't fail if it errors (mission deletion should still succeed) try { const n8nResult = await n8nService.triggerMissionDeletion(n8nData); logger.debug('N8N deletion webhook result', { success: n8nResult.success, hasError: !!n8nResult.error, missionId }); if (!n8nResult.success) { logger.warn('N8N deletion webhook returned error (mission still deleted)', { error: n8nResult.error, missionId }); } } catch (n8nError) { logger.error('N8N deletion webhook failed (mission still deleted from database)', { error: n8nError instanceof Error ? n8nError.message : String(n8nError), errorType: n8nError instanceof Error ? n8nError.name : typeof n8nError, missionId }); } // Step 4: Delete from database (cascade will handle related records) await prisma.mission.delete({ where: { id: missionId } }); logger.debug('Mission deleted successfully', { missionId }); return NextResponse.json({ success: true, message: 'Mission deleted successfully' }); } catch (error: any) { // Get missionId from params safely let errorMissionId = 'unknown'; try { const paramsResult = await params; errorMissionId = paramsResult.missionId; } catch (paramsError) { // If we can't get params, use unknown logger.warn('Could not get missionId from params in error handler'); } logger.error('Error deleting mission', { error: error instanceof Error ? error.message : String(error), errorStack: error instanceof Error ? error.stack : undefined, missionId: errorMissionId }); return NextResponse.json( { error: 'Failed to delete mission', details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } }