289 lines
9.4 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
}
|