import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from "@/app/api/auth/options"; import { listUserObjects, putObject, deleteObject } from '@/lib/s3'; // Error types for better error handling enum StorageError { UNAUTHORIZED = 'UNAUTHORIZED', FORBIDDEN = 'FORBIDDEN', NOT_FOUND = 'NOT_FOUND', VALIDATION_ERROR = 'VALIDATION_ERROR', S3_ERROR = 'S3_ERROR', INTERNAL_ERROR = 'INTERNAL_ERROR' } // Helper function to create error response function createErrorResponse( error: StorageError, message: string, status: number, details?: any ) { return NextResponse.json( { error: error, message: message, ...(details && { details }) }, { status } ); } // 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 list files in a folder export async function GET(request: Request) { try { const { authorized, userId } = await checkAuth(request); if (!authorized || !userId) { return createErrorResponse( StorageError.UNAUTHORIZED, 'Authentication required', 401 ); } const { searchParams } = new URL(request.url); const folderParam = searchParams.get('folder'); if (!folderParam) { return createErrorResponse( StorageError.VALIDATION_ERROR, 'Folder parameter is required', 400, { missing: 'folder' } ); } // Validate folder name (prevent path traversal) if (folderParam.includes('..') || folderParam.includes('/')) { return createErrorResponse( StorageError.VALIDATION_ERROR, 'Invalid folder name', 400, { folder: folderParam } ); } // Try both lowercase and original case to maintain compatibility // MinIO/S3 is case-sensitive, so we need to handle both formats const normalizedFolder = folderParam.toLowerCase(); console.log(`Listing files for user ${userId} in folder: ${folderParam} (normalized: ${normalizedFolder})`); try { // First try with the exact folder name as provided let files = await listUserObjects(userId, folderParam); // If no files found with original case, try with lowercase if (files.length === 0 && folderParam !== normalizedFolder) { console.log(`No files found with original case, trying lowercase: ${normalizedFolder}`); files = await listUserObjects(userId, normalizedFolder); } return NextResponse.json(files); } catch (s3Error) { console.error('S3 error listing files:', s3Error); return createErrorResponse( StorageError.S3_ERROR, 'Failed to list files from storage', 503, { error: s3Error instanceof Error ? s3Error.message : String(s3Error), folder: folderParam } ); } } catch (error) { console.error('Error listing files:', error); return createErrorResponse( StorageError.INTERNAL_ERROR, 'An unexpected error occurred', 500, { error: error instanceof Error ? error.message : String(error) } ); } } // POST endpoint to create a new file export async function POST(request: Request) { try { const { authorized, userId } = await checkAuth(request); if (!authorized || !userId) { return createErrorResponse( StorageError.UNAUTHORIZED, 'Authentication required', 401 ); } let body; try { body = await request.json(); } catch (parseError) { return createErrorResponse( StorageError.VALIDATION_ERROR, 'Invalid JSON in request body', 400 ); } const { title, content, folder } = body; if (!title || !content || !folder) { return createErrorResponse( StorageError.VALIDATION_ERROR, 'Missing required fields', 400, { received: { title: !!title, content: !!content, folder: !!folder }, required: ['title', 'content', 'folder'] } ); } // Validate inputs if (typeof title !== 'string' || title.trim().length === 0) { return createErrorResponse( StorageError.VALIDATION_ERROR, 'Title must be a non-empty string', 400 ); } if (typeof content !== 'string') { return createErrorResponse( StorageError.VALIDATION_ERROR, 'Content must be a string', 400 ); } // Validate folder name (prevent path traversal) if (folder.includes('..') || folder.includes('/')) { return createErrorResponse( StorageError.VALIDATION_ERROR, 'Invalid folder name', 400, { folder } ); } // Normalize folder name const normalizedFolder = folder.toLowerCase(); // Sanitize title (remove dangerous characters) const sanitizedTitle = title.replace(/[^a-zA-Z0-9._-]/g, '_'); // Create the full key (path) for the S3 object const key = `user-${userId}/${normalizedFolder}/${sanitizedTitle}${sanitizedTitle.endsWith('.md') ? '' : '.md'}`; console.log('Creating file in S3:', { key, contentLength: content.length }); try { // Save the file to S3 const file = await putObject(key, content); return NextResponse.json(file); } catch (s3Error) { console.error('S3 error creating file:', s3Error); return createErrorResponse( StorageError.S3_ERROR, 'Failed to create file in storage', 503, { error: s3Error instanceof Error ? s3Error.message : String(s3Error), key } ); } } catch (error) { console.error('Error creating file:', error); return createErrorResponse( StorageError.INTERNAL_ERROR, 'An unexpected error occurred', 500, { error: error instanceof Error ? error.message : String(error) } ); } } // PUT endpoint to update an existing file export async function PUT(request: Request) { try { const { authorized, userId } = await checkAuth(request); if (!authorized || !userId) { return createErrorResponse( StorageError.UNAUTHORIZED, 'Authentication required', 401 ); } let body; try { body = await request.json(); } catch (parseError) { return createErrorResponse( StorageError.VALIDATION_ERROR, 'Invalid JSON in request body', 400 ); } const { id, title, content, folder, mime } = body; // Check if this is using the direct id (key) or needs to construct one let key: string; const normalizedFolder = folder?.toLowerCase() || ''; const isDiaryOrHealth = normalizedFolder === 'diary' || normalizedFolder === 'health'; // For Diary/Health, if id is provided and it's in the correct format, use it // Otherwise, reconstruct from title (for migration from old format) if (id && id.includes(`user-${userId}/`)) { // Validate that the id is for the correct folder if (id.includes(`/${normalizedFolder}/`)) { // For Diary/Health, verify the id matches the expected format if (isDiaryOrHealth) { // Check if the id already matches the sanitized title format const sanitizedTitle = title ? title.replace(/[^a-zA-Z0-9._-]/g, '_') : ''; const expectedKey = `user-${userId}/${normalizedFolder}/${sanitizedTitle}${sanitizedTitle.endsWith('.md') ? '' : '.md'}`; // If id matches expected format, use it (this is an update to existing file) if (id === expectedKey) { key = id; console.log(`[PUT] Using provided id for Diary/Health: ${key}`); } else { // Id doesn't match expected format, but it's a valid id for this folder // Use it anyway (this handles updates to existing files with old format) key = id; console.log(`[PUT] Using provided id for Diary/Health (format mismatch, but valid): ${key}`); } } else { // For non-Diary/Health folders, use the provided id key = id; } } else { return createErrorResponse( StorageError.FORBIDDEN, 'File id does not match the specified folder', 403, { fileId: id, folder: normalizedFolder } ); } } else { // If id is not provided or invalid, construct it from folder and title if (!title || !folder) { return createErrorResponse( StorageError.VALIDATION_ERROR, 'Missing required fields: either id or (title and folder) must be provided', 400, { received: { title: !!title, folder: !!folder, id: !!id } } ); } // Validate folder name if (folder.includes('..') || folder.includes('/')) { return createErrorResponse( StorageError.VALIDATION_ERROR, 'Invalid folder name', 400, { folder } ); } // Sanitize title (spaces and special chars become underscores) // This converts "16 janvier 2026" to "16_janvier_2026" const sanitizedTitle = title.replace(/[^a-zA-Z0-9._-]/g, '_'); key = `user-${userId}/${normalizedFolder}/${sanitizedTitle}${sanitizedTitle.endsWith('.md') ? '' : '.md'}`; console.log(`[PUT] Constructed key from title for ${normalizedFolder}: ${key}`); } // Validate content if (content === undefined || content === null) { return createErrorResponse( StorageError.VALIDATION_ERROR, 'Content is required', 400 ); } console.log('Updating file in S3:', { key, contentLength: content?.length }); try { // Update the file const file = await putObject(key, content, mime); return NextResponse.json(file); } catch (s3Error) { console.error('S3 error updating file:', s3Error); return createErrorResponse( StorageError.S3_ERROR, 'Failed to update file in storage', 503, { error: s3Error instanceof Error ? s3Error.message : String(s3Error), key } ); } } catch (error) { console.error('Error updating file:', error); return createErrorResponse( StorageError.INTERNAL_ERROR, 'An unexpected error occurred', 500, { error: error instanceof Error ? error.message : String(error) } ); } } // DELETE endpoint to delete a file export async function DELETE(request: Request) { try { const { authorized, userId } = await checkAuth(request); if (!authorized || !userId) { return createErrorResponse( StorageError.UNAUTHORIZED, 'Authentication required', 401 ); } const { searchParams } = new URL(request.url); const id = searchParams.get('id'); if (!id) { return createErrorResponse( StorageError.VALIDATION_ERROR, 'Missing file id parameter', 400, { missing: 'id' } ); } // Ensure the user can only delete their own files if (!id.includes(`user-${userId}/`)) { return createErrorResponse( StorageError.FORBIDDEN, 'Unauthorized access to file', 403, { fileId: id } ); } console.log('Deleting file from S3:', { key: id }); try { // Delete the file await deleteObject(id); return NextResponse.json({ success: true }); } catch (s3Error) { console.error('S3 error deleting file:', s3Error); // Check if file doesn't exist (404) if (s3Error instanceof Error && s3Error.message.includes('NoSuchKey')) { return createErrorResponse( StorageError.NOT_FOUND, 'File not found', 404, { fileId: id } ); } return createErrorResponse( StorageError.S3_ERROR, 'Failed to delete file from storage', 503, { error: s3Error instanceof Error ? s3Error.message : String(s3Error), fileId: id } ); } } catch (error) { console.error('Error deleting file:', error); return createErrorResponse( StorageError.INTERNAL_ERROR, 'An unexpected error occurred', 500, { error: error instanceof Error ? error.message : String(error) } ); } }