Pages corrections pages missions

This commit is contained in:
alma 2026-01-16 14:19:55 +01:00
parent 6fc50e1765
commit 312ed5894d

View File

@ -5,26 +5,20 @@ import { prisma } from '@/lib/prisma';
import { S3Client, ListObjectsV2Command, GetObjectCommand, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
import { Readable } from 'stream';
// S3 Configuration for missions bucket
// Use the same configuration as mission-uploads.ts for consistency
const MISSIONS_S3_CONFIG = {
endpoint: 'https://dome-api.slm-lab.net',
region: 'us-east-1',
bucket: 'missions',
accessKey: process.env.MINIO_ACCESS_KEY || '4aBT4CMb7JIMMyUtp4Pl',
secretKey: process.env.MINIO_SECRET_KEY || 'HGn39XhCIlqOjmDVzRK9MED2Fci2rYvDDgbLFElg'
};
// Use the exact same S3 client configuration as mission-uploads.ts and image route
// This ensures we use the same credentials and settings that work for other mission operations
const missionsS3Client = new S3Client({
region: MISSIONS_S3_CONFIG.region,
endpoint: MISSIONS_S3_CONFIG.endpoint,
region: 'us-east-1',
endpoint: 'https://dome-api.slm-lab.net',
credentials: {
accessKeyId: MISSIONS_S3_CONFIG.accessKey,
secretAccessKey: MISSIONS_S3_CONFIG.secretKey
accessKeyId: '4aBT4CMb7JIMMyUtp4Pl',
secretAccessKey: 'HGn39XhCIlqOjmDVzRK9MED2Fci2rYvDDgbLFElg'
},
forcePathStyle: true // Required for MinIO
});
const MISSIONS_BUCKET = 'missions';
// Helper function to check if user has access to mission
async function checkMissionAccess(userId: string, missionId: string): Promise<boolean> {
const mission = await prisma.mission.findFirst({
@ -71,96 +65,110 @@ export async function GET(
const { searchParams } = new URL(request.url);
const path = searchParams.get('path') || ''; // Subfolder path within mission
// Construct prefix for listing
// Based on mission-uploads.ts, files are stored in MinIO without the "missions/" prefix
// The filePath in DB is "missions/{missionId}/attachments/{filename}"
// But in MinIO it's stored as "{missionId}/attachments/{filename}"
// So we need to use just "{missionId}/" or "{missionId}/{path}/" as prefix
const prefix = path ? `${missionId}/${path}/` : `${missionId}/`;
console.log(`[GET /api/missions/${missionId}/files] Listing with prefix: "${prefix}" in bucket: "${MISSIONS_S3_CONFIG.bucket}"`);
const command = new ListObjectsV2Command({
Bucket: MISSIONS_S3_CONFIG.bucket,
Prefix: prefix,
Delimiter: '/'
// Get attachments from database first (like the missions page does)
const attachments = await prisma.attachment.findMany({
where: { missionId },
select: {
id: true,
filename: true,
filePath: true,
fileType: true,
fileSize: true,
createdAt: true
},
orderBy: { createdAt: 'desc' }
});
console.log(`[GET /api/missions/${missionId}/files] S3 command:`, {
bucket: MISSIONS_S3_CONFIG.bucket,
prefix: prefix,
endpoint: MISSIONS_S3_CONFIG.endpoint,
accessKey: MISSIONS_S3_CONFIG.accessKey.substring(0, 4) + '...'
});
let response;
try {
response = await missionsS3Client.send(command);
console.log(`[GET /api/missions/${missionId}/files] S3 response success:`, {
commonPrefixes: response.CommonPrefixes?.length || 0,
contents: response.Contents?.length || 0
});
} catch (s3Error: any) {
console.error(`[GET /api/missions/${missionId}/files] S3 error:`, {
code: s3Error.Code,
message: s3Error.message,
bucket: MISSIONS_S3_CONFIG.bucket,
prefix: prefix,
endpoint: MISSIONS_S3_CONFIG.endpoint,
fullError: s3Error
});
throw s3Error;
}
// Try to list files from S3, but don't fail if it doesn't work
let s3Folders: any[] = [];
let s3Files: any[] = [];
// Extract folders (CommonPrefixes)
const folders = (response.CommonPrefixes || []).map(commonPrefix => {
const folderPath = commonPrefix.Prefix || '';
// Extract folder name from path (e.g., "123/docs/" -> "docs")
const pathParts = folderPath.replace(prefix, '').split('/').filter(Boolean);
const folderName = pathParts[pathParts.length - 1] || folderPath;
// Store full path with missions/ prefix for consistency
const fullPath = `missions/${folderPath}`;
return {
type: 'folder',
name: folderName,
path: fullPath,
key: fullPath
};
});
try {
// Construct prefix for listing
// Based on mission-uploads.ts, files are stored in MinIO without the "missions/" prefix
// The filePath in DB is "missions/{missionId}/attachments/{filename}"
// But in MinIO it's stored as "{missionId}/attachments/{filename}"
const prefix = path ? `${missionId}/${path}/` : `${missionId}/`;
// Extract files (Contents, excluding folder markers)
const files = (response.Contents || [])
.filter(obj => {
if (!obj.Key) return false;
// Exclude folder markers
if (obj.Key.endsWith('/')) return false;
// Exclude placeholder files
if (obj.Key.includes('.placeholder')) return false;
return true;
})
.map(obj => {
const key = obj.Key || '';
// Store full path with missions/ prefix for consistency
const fullPath = `missions/${key}`;
console.log(`[GET /api/missions/${missionId}/files] Listing with prefix: "${prefix}" in bucket: "${MISSIONS_BUCKET}"`);
const command = new ListObjectsV2Command({
Bucket: MISSIONS_BUCKET,
Prefix: prefix,
Delimiter: '/'
});
const response = await missionsS3Client.send(command);
// Extract folders (CommonPrefixes)
s3Folders = (response.CommonPrefixes || []).map(commonPrefix => {
const folderPath = commonPrefix.Prefix || '';
const pathParts = folderPath.replace(prefix, '').split('/').filter(Boolean);
const folderName = pathParts[pathParts.length - 1] || folderPath;
const fullPath = `missions/${folderPath}`;
return {
type: 'file',
name: key.split('/').pop() || key,
type: 'folder',
name: folderName,
path: fullPath,
key: fullPath,
size: obj.Size,
lastModified: obj.LastModified
key: fullPath
};
});
// Extract files (Contents, excluding folder markers)
s3Files = (response.Contents || [])
.filter(obj => {
if (!obj.Key) return false;
if (obj.Key.endsWith('/')) return false;
if (obj.Key.includes('.placeholder')) return false;
return true;
})
.map(obj => {
const key = obj.Key || '';
const fullPath = `missions/${key}`;
return {
type: 'file',
name: key.split('/').pop() || key,
path: fullPath,
key: fullPath,
size: obj.Size,
lastModified: obj.LastModified
};
});
} catch (s3Error: any) {
console.warn(`[GET /api/missions/${missionId}/files] S3 listing failed, using database attachments only:`, {
code: s3Error.Code,
message: s3Error.message
});
// Continue with database attachments only
}
// Convert database attachments to file format
const dbFiles = attachments.map(att => ({
type: 'file' as const,
name: att.filename,
path: att.filePath,
key: att.filePath,
size: att.fileSize,
lastModified: att.createdAt
}));
// Combine S3 files and database attachments, removing duplicates
const allFiles = [...s3Files, ...dbFiles];
const uniqueFiles = allFiles.filter((file, index, self) =>
index === self.findIndex(f => f.key === file.key)
);
// Combine folders and files
const allItems = [...s3Folders, ...uniqueFiles].sort((a, b) => {
if (a.type !== b.type) {
return a.type === 'folder' ? -1 : 1;
}
return a.name.localeCompare(b.name);
});
return NextResponse.json({
folders,
files: [...folders, ...files].sort((a, b) => {
// Folders first, then files, both alphabetically
if (a.type !== b.type) {
return a.type === 'folder' ? -1 : 1;
}
return a.name.localeCompare(b.name);
})
folders: s3Folders,
files: allItems
});
} catch (error) {
console.error('Error listing mission files:', error);
@ -207,7 +215,7 @@ export async function POST(
const minioKey = key.replace(/^missions\//, '');
const command = new GetObjectCommand({
Bucket: MISSIONS_S3_CONFIG.bucket,
Bucket: MISSIONS_BUCKET,
Key: minioKey
});
@ -260,7 +268,7 @@ export async function PUT(
const minioKey = key.replace(/^missions\//, '');
const command = new PutObjectCommand({
Bucket: MISSIONS_S3_CONFIG.bucket,
Bucket: MISSIONS_BUCKET,
Key: minioKey,
Body: content || Buffer.alloc(0),
ContentType: 'text/plain'