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 ? (

{
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