From 76d4d55285e3a68432d2857b317b4e07e505e16e Mon Sep 17 00:00:00 2001 From: alma Date: Tue, 6 May 2025 10:51:17 +0200 Subject: [PATCH] missions carrousse --- app/api/missions/[missionId]/route.ts | 13 ++- app/api/missions/upload/route.ts | 139 +++++++++++++++------- app/missions/page.tsx | 4 +- components/missions/file-upload.tsx | 34 +++++- lib/mission-uploads.ts | 26 ++++- lib/s3.ts | 119 ++++++++++++++++++- scripts/test-minio-upload.js | 160 ++++++++++++++++++++++++++ 7 files changed, 445 insertions(+), 50 deletions(-) create mode 100644 scripts/test-minio-upload.js diff --git a/app/api/missions/[missionId]/route.ts b/app/api/missions/[missionId]/route.ts index b0776f44..94b6668d 100644 --- a/app/api/missions/[missionId]/route.ts +++ b/app/api/missions/[missionId]/route.ts @@ -3,6 +3,7 @@ import { getServerSession } from 'next-auth'; import { authOptions } from "@/app/api/auth/options"; import { prisma } from '@/lib/prisma'; import { deleteMissionLogo } from '@/lib/mission-uploads'; +import { getPublicUrl } from '@/lib/s3'; // Helper function to check authentication async function checkAuth(request: Request) { @@ -77,8 +78,18 @@ export async function GET(request: Request, props: { params: Promise<{ missionId if (!mission) { return NextResponse.json({ error: 'Mission not found or access denied' }, { status: 404 }); } + + // Add public URLs to mission logo and attachments + const missionWithUrls = { + ...mission, + logoUrl: mission.logo ? getPublicUrl(mission.logo) : null, + attachments: mission.attachments.map((attachment: { id: string; filename: string; filePath: string; fileType: string; fileSize: number; createdAt: Date }) => ({ + ...attachment, + publicUrl: getPublicUrl(attachment.filePath) + })) + }; - return NextResponse.json(mission); + return NextResponse.json(missionWithUrls); } catch (error) { console.error('Error retrieving mission:', error); return NextResponse.json({ diff --git a/app/api/missions/upload/route.ts b/app/api/missions/upload/route.ts index e6d080e0..abe04504 100644 --- a/app/api/missions/upload/route.ts +++ b/app/api/missions/upload/route.ts @@ -9,6 +9,7 @@ import { generateMissionLogoUploadUrl, generateMissionAttachmentUploadUrl } from '@/lib/mission-uploads'; +import { getPublicUrl } from '@/lib/s3'; // Helper function to check authentication async function checkAuth(request: Request) { @@ -84,19 +85,35 @@ export async function GET(request: Request) { // Handle file upload (server-side) export async function POST(request: Request) { + console.log('=== File upload request received ==='); + try { + console.log('Checking authentication...'); const { authorized, userId } = await checkAuth(request); if (!authorized || !userId) { + console.log('Authentication failed'); return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } + console.log('User authenticated:', userId); // Parse the form data + console.log('Parsing form data...'); const formData = await request.formData(); const missionId = formData.get('missionId') as string; const type = formData.get('type') as string; // 'logo' or 'attachment' const file = formData.get('file') as File; + + console.log('Form data received:', { + missionId, + type, + fileExists: !!file, + fileName: file?.name, + fileSize: file?.size, + fileType: file?.type + }); if (!missionId || !type || !file) { + console.log('Missing required fields:', { missionId: !!missionId, type: !!type, file: !!file }); return NextResponse.json({ error: 'Missing required fields', required: { missionId: true, type: true, file: true }, @@ -105,69 +122,111 @@ export async function POST(request: Request) { } // Verify that the mission exists and user has access to it + console.log('Verifying mission access...'); const mission = await prisma.mission.findUnique({ where: { id: missionId }, select: { id: true, creatorId: true } }); if (!mission) { + console.log('Mission not found:', missionId); return NextResponse.json({ error: 'Mission not found' }, { status: 404 }); } // Currently only allow creator to upload files if (mission.creatorId !== userId) { + console.log('User not authorized to upload to this mission', { userId, creatorId: mission.creatorId }); return NextResponse.json({ error: 'Not authorized to upload to this mission' }, { status: 403 }); } + console.log('Mission access verified'); if (type === 'logo') { - // Upload logo file to Minio - const { filePath } = await uploadMissionLogo(userId, missionId, file); - - // Update mission record with logo path - await prisma.mission.update({ - where: { id: missionId }, - data: { logo: filePath } - }); - - return NextResponse.json({ success: true, filePath }); + console.log('Processing logo upload...'); + try { + // Upload logo file to Minio + const { filePath } = await uploadMissionLogo(userId, missionId, file); + console.log('Logo uploaded successfully to path:', filePath); + + // Generate public URL + const publicUrl = getPublicUrl(filePath); + console.log('Public URL for logo:', publicUrl); + + // Update mission record with logo path + console.log('Updating mission record with logo path...'); + await prisma.mission.update({ + where: { id: missionId }, + data: { logo: filePath } + }); + console.log('Mission record updated'); + + return NextResponse.json({ + success: true, + filePath, + publicUrl + }); + } catch (logoError) { + console.error('Error in logo upload process:', logoError); + return NextResponse.json({ + error: 'Logo upload failed', + details: logoError instanceof Error ? logoError.message : String(logoError) + }, { status: 500 }); + } } else if (type === 'attachment') { // Upload attachment file to Minio - const { filename, filePath, fileType, fileSize } = await uploadMissionAttachment( - userId, - missionId, - file - ); - - // Create attachment record in database - const attachment = await prisma.attachment.create({ - data: { - filename, - filePath, - fileType, - fileSize, - missionId, - uploaderId: userId - } - }); - - return NextResponse.json({ - success: true, - attachment: { - id: attachment.id, - filename: attachment.filename, - filePath: attachment.filePath, - fileType: attachment.fileType, - fileSize: attachment.fileSize, - createdAt: attachment.createdAt - } - }); + console.log('Processing attachment upload...'); + try { + const { filename, filePath, fileType, fileSize } = await uploadMissionAttachment( + userId, + missionId, + file + ); + console.log('Attachment uploaded successfully to path:', filePath); + + // Generate public URL + const publicUrl = getPublicUrl(filePath); + console.log('Public URL for attachment:', publicUrl); + + // Create attachment record in database + console.log('Creating attachment record in database...'); + const attachment = await prisma.attachment.create({ + data: { + filename, + filePath, + fileType, + fileSize, + missionId, + uploaderId: userId + } + }); + console.log('Attachment record created:', attachment.id); + + return NextResponse.json({ + success: true, + attachment: { + id: attachment.id, + filename: attachment.filename, + filePath: attachment.filePath, + publicUrl, + fileType: attachment.fileType, + fileSize: attachment.fileSize, + createdAt: attachment.createdAt + } + }); + } catch (attachmentError) { + console.error('Error in attachment upload process:', attachmentError); + return NextResponse.json({ + error: 'Attachment upload failed', + details: attachmentError instanceof Error ? attachmentError.message : String(attachmentError) + }, { status: 500 }); + } } else { + console.log('Invalid upload type:', type); return NextResponse.json({ error: 'Invalid upload type' }, { status: 400 }); } } catch (error) { - console.error('Error uploading file:', error); + console.error('Unhandled error in upload process:', error); return NextResponse.json({ error: 'Internal server error', details: error instanceof Error ? error.message : String(error) diff --git a/app/missions/page.tsx b/app/missions/page.tsx index 7fb8ca8f..2d16a67a 100644 --- a/app/missions/page.tsx +++ b/app/missions/page.tsx @@ -6,6 +6,7 @@ import { Input } from "@/components/ui/input"; import Link from "next/link"; import { Button } from "@/components/ui/button"; import { useToast } from "@/components/ui/use-toast"; +import { getPublicUrl } from "@/lib/s3"; // Define Mission interface interface User { @@ -198,11 +199,12 @@ export default function MissionsPage() {
{mission.logo ? ( {mission.name} { console.log("Logo failed to load:", mission.logo); + console.log("Full URL attempted:", mission.logo ? getPublicUrl(mission.logo) : 'undefined logo'); // If the image fails to load, show the fallback (e.currentTarget as HTMLImageElement).style.display = 'none'; // Show the fallback div diff --git a/components/missions/file-upload.tsx b/components/missions/file-upload.tsx index 11a66071..bbaf7c8b 100644 --- a/components/missions/file-upload.tsx +++ b/components/missions/file-upload.tsx @@ -92,7 +92,23 @@ export function FileUpload({ }; const handleUpload = async () => { - if (!file || !session?.user?.id || !missionId) return; + if (!file || !session?.user?.id || !missionId) { + console.error('Upload failed: Missing required data', { + fileExists: !!file, + userIdExists: !!session?.user?.id, + missionIdExists: !!missionId + }); + return; + } + + console.log('Starting upload process...', { + fileName: file.name, + fileSize: file.size, + fileType: file.type, + missionId, + userId: session.user.id, + uploadType: type + }); setIsUploading(true); setProgress(0); @@ -104,18 +120,28 @@ export function FileUpload({ formData.append('missionId', missionId); formData.append('type', type); + console.log('FormData prepared, sending to API...'); + // Upload the file const response = await fetch('/api/missions/upload', { method: 'POST', body: formData }); + console.log('API response received:', { + status: response.status, + statusText: response.statusText, + ok: response.ok + }); + if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Upload failed'); + const errorData = await response.json(); + console.error('API returned error:', errorData); + throw new Error(errorData.error || 'Upload failed'); } const result = await response.json(); + console.log('Upload successful, result:', result); setProgress(100); // Reset file after successful upload @@ -136,7 +162,7 @@ export function FileUpload({ }); }, 1000); } catch (error) { - console.error('Upload error:', error); + console.error('Upload error details:', error); setIsUploading(false); toast({ title: 'Upload failed', diff --git a/lib/mission-uploads.ts b/lib/mission-uploads.ts index 6054d78c..110f06e5 100644 --- a/lib/mission-uploads.ts +++ b/lib/mission-uploads.ts @@ -22,17 +22,33 @@ export async function uploadMissionLogo( file: File ): Promise<{ filePath: string }> { try { + console.log('=== Starting logo upload process ==='); + console.log('Upload params:', { userId, missionId, fileName: file.name, fileSize: file.size, fileType: file.type }); + // Get file extension const fileExtension = file.name.substring(file.name.lastIndexOf('.')); + console.log('File extension:', fileExtension); // Create file path const filePath = getMissionLogoPath(userId, missionId, fileExtension); + console.log('Generated file path:', filePath); // Convert file to ArrayBuffer + console.log('Converting file to buffer...'); const arrayBuffer = await file.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); + console.log('Buffer created, size:', buffer.length); // Upload to Minio + console.log('Creating S3 command with bucket:', S3_CONFIG.bucket); + console.log('S3 config:', { + endpoint: S3_CONFIG.endpoint || 'MISSING!', + region: S3_CONFIG.region || 'MISSING!', + bucket: S3_CONFIG.bucket || 'MISSING!', + hasAccessKey: !!S3_CONFIG.accessKey || 'MISSING!', + hasSecretKey: !!S3_CONFIG.secretKey || 'MISSING!' + }); + const command = new PutObjectCommand({ Bucket: S3_CONFIG.bucket, Key: filePath, @@ -40,8 +56,16 @@ export async function uploadMissionLogo( ContentType: file.type, }); - await s3Client.send(command); + console.log('Sending upload command to S3/Minio...'); + try { + const result = await s3Client.send(command); + console.log('Upload successful, result:', result); + } catch (uploadError) { + console.error('S3 upload error details:', uploadError); + throw uploadError; + } + console.log('Upload complete, returning file path:', filePath); return { filePath }; } catch (error) { console.error('Error uploading mission logo:', error); diff --git a/lib/s3.ts b/lib/s3.ts index 1261f18d..b68cae75 100644 --- a/lib/s3.ts +++ b/lib/s3.ts @@ -44,6 +44,27 @@ if (S3_CONFIG.accessKey && S3_CONFIG.secretKey) { // Create S3 client export const s3Client = new S3Client(s3Config); +// Check Minio connection on startup +(async () => { + try { + console.log('Testing Minio/S3 connection...'); + // Try a simple operation to test the connection + const command = new ListObjectsV2Command({ + Bucket: S3_CONFIG.bucket, + MaxKeys: 1 + }); + + const response = await s3Client.send(command); + console.log('Minio/S3 connection successful! Bucket exists and is accessible.'); + console.log(`Bucket details: ${S3_CONFIG.bucket}, contains ${response.KeyCount || 0} objects`); + } catch (error) { + console.error('CRITICAL ERROR: Failed to connect to Minio/S3 server!'); + console.error('File uploads will fail until this is resolved.'); + console.error('Error details:', error); + console.error('Please check your S3/Minio configuration and server status.'); + } +})(); + // Check for required environment variables if (!S3_CONFIG.endpoint || !S3_CONFIG.bucket) { console.error('ERROR: Missing required S3 environment variables!'); @@ -119,8 +140,14 @@ export async function getObjectContent(key: string) { } // Put object (create or update a file) -export async function putObject(key: string, content: string, contentType?: string) { +export async function putObject(key: string, content: string | Buffer, contentType?: string) { try { + console.log(`Attempting to upload to S3/Minio: ${key}`); + + if (!S3_CONFIG.bucket) { + throw new Error('S3 bucket name is not configured'); + } + const command = new PutObjectCommand({ Bucket: S3_CONFIG.bucket, Key: key, @@ -128,19 +155,25 @@ export async function putObject(key: string, content: string, contentType?: stri ContentType: contentType || (key.endsWith('.md') ? 'text/markdown' : 'text/plain') }); + console.log(`S3 PutObject request prepared for ${key}`); const response = await s3Client.send(command); + console.log(`S3 PutObject successful for ${key}, ETag: ${response.ETag}`); return { id: key, title: key.split('/').pop()?.replace('.md', '') || '', lastModified: new Date().toISOString(), - size: content.length, + size: typeof content === 'string' ? content.length : content.length, type: 'file', mime: contentType || (key.endsWith('.md') ? 'text/markdown' : 'text/plain'), etag: response.ETag }; } catch (error) { - console.error('Error putting object:', error); + console.error(`Error putting object to S3/Minio (${key}):`, error); + // Check for specific S3 errors + if ((error as any)?.name === 'NoSuchBucket') { + console.error(`Bucket "${S3_CONFIG.bucket}" does not exist. Please create it first.`); + } throw error; } } @@ -211,4 +244,84 @@ export async function generatePresignedUrl(key: string, expiresIn = 3600) { console.error('Error generating presigned URL:', error); throw error; } +} + +// Generate a public URL for a file stored in Minio/S3 +export function getPublicUrl(filePath: string): string { + if (!filePath) return ''; + if (filePath.startsWith('http')) return filePath; // Already a full URL + + console.log('Generating public URL for:', filePath); + + // Remove leading slash if present + const cleanPath = filePath.startsWith('/') ? filePath.substring(1) : filePath; + + // Construct the full URL + const endpoint = S3_CONFIG.endpoint?.replace(/\/$/, ''); // Remove trailing slash if present + const bucket = S3_CONFIG.bucket; + + console.log('S3 Config for URL generation:', { + endpoint, + bucket, + cleanPath + }); + + // Return original path if no endpoint is configured + if (!endpoint) { + console.warn('No S3/Minio endpoint configured, returning original path'); + return cleanPath; + } + + // Construct and return the full URL + const publicUrl = `${endpoint}/${bucket}/${cleanPath}`; + console.log('Generated public URL:', publicUrl); + return publicUrl; +} + +// Test Minio connection - can be called from browser console +export async function testMinioConnection() { + console.log('=== Testing Minio Connection ==='); + try { + console.log('S3 Configuration:', { + endpoint: S3_CONFIG.endpoint || 'MISSING!', + region: S3_CONFIG.region || 'MISSING!', + bucket: S3_CONFIG.bucket || 'MISSING!', + hasAccessKey: !!S3_CONFIG.accessKey || 'MISSING!', + hasSecretKey: !!S3_CONFIG.secretKey || 'MISSING!', + }); + + // Try a simple operation + console.log('Attempting to list objects...'); + const command = new ListObjectsV2Command({ + Bucket: S3_CONFIG.bucket || '', + MaxKeys: 5 + }); + + const response = await s3Client.send(command); + console.log('Connection successful!'); + console.log('Response:', response); + + const files = response.Contents || []; + console.log(`Found ${files.length} files in bucket ${S3_CONFIG.bucket}:`); + files.forEach((file, index) => { + console.log(` ${index + 1}. ${file.Key} (${file.Size} bytes)`); + }); + + console.log('=== Test completed successfully ==='); + return { success: true, files: files.map(f => f.Key) }; + } catch (error) { + console.error('=== Test failed ==='); + console.error('Error details:', error); + return { success: false, error }; + } +} + +// Make testMinioConnection available globally if in browser +if (typeof window !== 'undefined') { + (window as any).testMinioConnection = testMinioConnection; + + // Also expose the getPublicUrl function + (window as any).getMinioUrl = getPublicUrl; + + console.log('Minio test utilities available. Run window.testMinioConnection() to test Minio connection.'); } \ No newline at end of file diff --git a/scripts/test-minio-upload.js b/scripts/test-minio-upload.js new file mode 100644 index 00000000..6c0046c2 --- /dev/null +++ b/scripts/test-minio-upload.js @@ -0,0 +1,160 @@ +/** + * Test Minio Upload Script + * + * Usage: + * node scripts/test-minio-upload.js + * + * This script tests connectivity to Minio by uploading a simple test file + * and then trying to retrieve it. It uses the same S3 client configuration + * as the main application. + */ + +require('dotenv').config(); +const { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3'); + +// Configuration for S3/Minio from environment variables +const S3_CONFIG = { + endpoint: process.env.MINIO_S3_UPLOAD_BUCKET_URL, + region: process.env.MINIO_AWS_REGION, + bucket: process.env.MINIO_AWS_S3_UPLOAD_BUCKET_NAME, + accessKey: process.env.MINIO_ACCESS_KEY || process.env.AWS_ACCESS_KEY_ID, + secretKey: process.env.MINIO_SECRET_KEY || process.env.AWS_SECRET_ACCESS_KEY, +}; + +// Initialize S3 client with standard configuration +const s3Config = { + region: S3_CONFIG.region, + endpoint: S3_CONFIG.endpoint, + forcePathStyle: true, // Required for MinIO +}; + +// Add credentials if available +if (S3_CONFIG.accessKey && S3_CONFIG.secretKey) { + Object.assign(s3Config, { + credentials: { + accessKeyId: S3_CONFIG.accessKey, + secretAccessKey: S3_CONFIG.secretKey + } + }); +} + +// Create S3 client +const s3Client = new S3Client(s3Config); + +/** + * Generate a public URL for a file stored in Minio/S3 + */ +function getPublicUrl(filePath) { + if (!filePath) return ''; + if (filePath.startsWith('http')) return filePath; // Already a full URL + + // Remove leading slash if present + const cleanPath = filePath.startsWith('/') ? filePath.substring(1) : filePath; + + // Construct the full URL + const endpoint = S3_CONFIG.endpoint?.replace(/\/$/, ''); // Remove trailing slash if present + const bucket = S3_CONFIG.bucket; + + // Return original path if no endpoint is configured + if (!endpoint) { + console.warn('No S3/Minio endpoint configured, returning original path'); + return cleanPath; + } + + // Construct and return the full URL + return `${endpoint}/${bucket}/${cleanPath}`; +} + +/** + * Test Minio connection by uploading and retrieving a test file + */ +async function testMinioUpload() { + // Generate a unique test file path + const testKey = `test/upload-test-${Date.now()}.txt`; + const testContent = 'This is a test file created at ' + new Date().toISOString(); + + console.log('=== Testing Minio Upload ==='); + console.log('S3 Configuration:'); + console.log(' Endpoint:', S3_CONFIG.endpoint || 'MISSING!'); + console.log(' Region:', S3_CONFIG.region || 'MISSING!'); + console.log(' Bucket:', S3_CONFIG.bucket || 'MISSING!'); + console.log(' Has Access Key:', !!S3_CONFIG.accessKey || 'MISSING!'); + console.log(' Has Secret Key:', !!S3_CONFIG.secretKey || 'MISSING!'); + + try { + // 1. Upload test file + console.log('\nStep 1: Uploading test file...'); + console.log(' File path:', testKey); + console.log(' Content:', testContent); + + const uploadCommand = new PutObjectCommand({ + Bucket: S3_CONFIG.bucket, + Key: testKey, + Body: testContent, + ContentType: 'text/plain' + }); + + const uploadResult = await s3Client.send(uploadCommand); + console.log(' Upload successful!'); + console.log(' ETag:', uploadResult.ETag); + + // 2. Generate public URL + const publicUrl = getPublicUrl(testKey); + console.log('\nStep 2: Generated public URL:'); + console.log(' URL:', publicUrl); + + // 3. Download the file to verify it was uploaded correctly + console.log('\nStep 3: Downloading test file to verify...'); + const downloadCommand = new GetObjectCommand({ + Bucket: S3_CONFIG.bucket, + Key: testKey + }); + + const downloadResult = await s3Client.send(downloadCommand); + const downloadedContent = await downloadResult.Body.transformToString(); + + console.log(' Download successful!'); + console.log(' Content:', downloadedContent); + + if (downloadedContent === testContent) { + console.log(' Content verification: ✓ Matches original'); + } else { + console.log(' Content verification: ✗ Does not match original'); + console.log(' Expected:', testContent); + console.log(' Received:', downloadedContent); + } + + // 4. Cleanup - Delete the test file + console.log('\nStep 4: Cleaning up - deleting test file...'); + const deleteCommand = new DeleteObjectCommand({ + Bucket: S3_CONFIG.bucket, + Key: testKey + }); + + await s3Client.send(deleteCommand); + console.log(' Deletion successful!'); + + console.log('\n=== Test completed successfully ==='); + console.log('Minio is properly configured and accessible.'); + console.log('File uploads should be working correctly.'); + + } catch (error) { + console.error('\n=== Test failed ==='); + console.error('Error details:', error); + console.error('\nPossible issues:'); + console.error('1. Minio server is not running or not accessible'); + console.error('2. Incorrect endpoint URL'); + console.error('3. Invalid credentials'); + console.error('4. Bucket does not exist or is not accessible'); + console.error('5. Network connectivity issues'); + + console.error('\nEnvironment variables to check:'); + console.error('- MINIO_S3_UPLOAD_BUCKET_URL'); + console.error('- MINIO_AWS_S3_UPLOAD_BUCKET_NAME'); + console.error('- MINIO_ACCESS_KEY / AWS_ACCESS_KEY_ID'); + console.error('- MINIO_SECRET_KEY / AWS_SECRET_ACCESS_KEY'); + } +} + +// Run the test +testMinioUpload(); \ No newline at end of file