NeahStable/app/api/missions/[missionId]/route.ts
2026-01-21 13:02:42 +01:00

289 lines
9.4 KiB
TypeScript

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 }
);
}
}