diff --git a/app/api/missions/[missionId]/attachments/route.ts b/app/api/missions/[missionId]/attachments/route.ts index 38cd65a8..3aa29a8e 100644 --- a/app/api/missions/[missionId]/attachments/route.ts +++ b/app/api/missions/[missionId]/attachments/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from "@/app/api/auth/options"; import { prisma } from '@/lib/prisma'; +import { getPublicUrl, S3_CONFIG } from '@/lib/s3'; // Helper function to check authentication async function checkAuth(request: Request) { @@ -60,7 +61,13 @@ export async function GET(request: Request, props: { params: Promise<{ missionId } }); - return NextResponse.json(attachments); + // Add public URLs to attachments + const attachmentsWithUrls = attachments.map(attachment => ({ + ...attachment, + publicUrl: getPublicUrl(attachment.filePath, S3_CONFIG.missionsBucket) + })); + + return NextResponse.json(attachmentsWithUrls); } catch (error) { console.error('Error fetching mission attachments:', error); return NextResponse.json({ diff --git a/app/api/missions/[missionId]/route.ts b/app/api/missions/[missionId]/route.ts index 94b6668d..480bbbea 100644 --- a/app/api/missions/[missionId]/route.ts +++ b/app/api/missions/[missionId]/route.ts @@ -3,7 +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'; +import { getPublicUrl, S3_CONFIG } from '@/lib/s3'; // Helper function to check authentication async function checkAuth(request: Request) { @@ -82,10 +82,10 @@ export async function GET(request: Request, props: { params: Promise<{ missionId // Add public URLs to mission logo and attachments const missionWithUrls = { ...mission, - logoUrl: mission.logo ? getPublicUrl(mission.logo) : null, + logoUrl: mission.logo ? getPublicUrl(mission.logo, S3_CONFIG.missionsBucket) : null, attachments: mission.attachments.map((attachment: { id: string; filename: string; filePath: string; fileType: string; fileSize: number; createdAt: Date }) => ({ ...attachment, - publicUrl: getPublicUrl(attachment.filePath) + publicUrl: getPublicUrl(attachment.filePath, S3_CONFIG.missionsBucket) })) }; diff --git a/app/api/missions/route.ts b/app/api/missions/route.ts index c49b8f61..c6ae164c 100644 --- a/app/api/missions/route.ts +++ b/app/api/missions/route.ts @@ -3,6 +3,7 @@ import { getServerSession } from 'next-auth'; import { authOptions } from "@/app/api/auth/options"; import { prisma } from '@/lib/prisma'; import { getPublicUrl } from '@/lib/s3'; +import { S3_CONFIG } from '@/lib/s3'; // Helper function to check authentication async function checkAuth(request: Request) { @@ -86,7 +87,7 @@ export async function GET(request: Request) { // Transform logo paths to public URLs const missionsWithPublicUrls = missions.map(mission => ({ ...mission, - logo: mission.logo ? getPublicUrl(mission.logo) : null + logo: mission.logo ? getPublicUrl(mission.logo, S3_CONFIG.missionsBucket) : null })); return NextResponse.json({ diff --git a/app/api/missions/upload/route.ts b/app/api/missions/upload/route.ts index abe04504..6a042d34 100644 --- a/app/api/missions/upload/route.ts +++ b/app/api/missions/upload/route.ts @@ -9,7 +9,7 @@ import { generateMissionLogoUploadUrl, generateMissionAttachmentUploadUrl } from '@/lib/mission-uploads'; -import { getPublicUrl } from '@/lib/s3'; +import { getPublicUrl, S3_CONFIG } from '@/lib/s3'; // Helper function to check authentication async function checkAuth(request: Request) { @@ -148,7 +148,7 @@ export async function POST(request: Request) { console.log('Logo uploaded successfully to path:', filePath); // Generate public URL - const publicUrl = getPublicUrl(filePath); + const publicUrl = getPublicUrl(filePath, S3_CONFIG.missionsBucket); console.log('Public URL for logo:', publicUrl); // Update mission record with logo path @@ -184,7 +184,7 @@ export async function POST(request: Request) { console.log('Attachment uploaded successfully to path:', filePath); // Generate public URL - const publicUrl = getPublicUrl(filePath); + const publicUrl = getPublicUrl(filePath, S3_CONFIG.missionsBucket); console.log('Public URL for attachment:', publicUrl); // Create attachment record in database diff --git a/lib/mission-uploads.ts b/lib/mission-uploads.ts index 110f06e5..11ca3dd2 100644 --- a/lib/mission-uploads.ts +++ b/lib/mission-uploads.ts @@ -1,5 +1,6 @@ import { s3Client, putObject, generatePresignedUrl, S3_CONFIG, deleteObject } from '@/lib/s3'; -import { PutObjectCommand } from '@aws-sdk/client-s3'; +import { PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; /** * Utilities for mission-related file uploads using Minio @@ -39,18 +40,18 @@ export async function uploadMissionLogo( 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); + // Upload to Minio using the missions bucket + console.log('Creating S3 command with bucket:', S3_CONFIG.missionsBucket); console.log('S3 config:', { endpoint: S3_CONFIG.endpoint || 'MISSING!', region: S3_CONFIG.region || 'MISSING!', - bucket: S3_CONFIG.bucket || 'MISSING!', + bucket: S3_CONFIG.missionsBucket || 'MISSING!', hasAccessKey: !!S3_CONFIG.accessKey || 'MISSING!', hasSecretKey: !!S3_CONFIG.secretKey || 'MISSING!' }); const command = new PutObjectCommand({ - Bucket: S3_CONFIG.bucket, + Bucket: S3_CONFIG.missionsBucket, Key: filePath, Body: buffer, ContentType: file.type, @@ -92,9 +93,9 @@ export async function uploadMissionAttachment( const arrayBuffer = await file.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); - // Upload to Minio + // Upload to Minio using missions bucket const command = new PutObjectCommand({ - Bucket: S3_CONFIG.bucket, + Bucket: S3_CONFIG.missionsBucket, Key: filePath, Body: buffer, ContentType: file.type, @@ -114,6 +115,21 @@ export async function uploadMissionAttachment( } } +// Generate presigned URL for missions bucket +async function generateMissionPresignedUrl(key: string, expiresIn = 3600): Promise { + try { + const command = new PutObjectCommand({ + Bucket: S3_CONFIG.missionsBucket, + Key: key + }); + + return await getSignedUrl(s3Client, command, { expiresIn }); + } catch (error) { + console.error('Error generating presigned URL for missions bucket:', error); + throw error; + } +} + // Generate presigned URL for direct browser upload of mission logo export async function generateMissionLogoUploadUrl( userId: string, @@ -126,7 +142,7 @@ export async function generateMissionLogoUploadUrl( }> { try { const filePath = getMissionLogoPath(userId, missionId, fileExtension); - const uploadUrl = await generatePresignedUrl(filePath, expiresIn); + const uploadUrl = await generateMissionPresignedUrl(filePath, expiresIn); return { uploadUrl, filePath }; } catch (error) { @@ -147,7 +163,7 @@ export async function generateMissionAttachmentUploadUrl( }> { try { const filePath = getMissionAttachmentPath(userId, missionId, filename); - const uploadUrl = await generatePresignedUrl(filePath, expiresIn); + const uploadUrl = await generateMissionPresignedUrl(filePath, expiresIn); return { uploadUrl, filePath }; } catch (error) { @@ -156,10 +172,26 @@ export async function generateMissionAttachmentUploadUrl( } } +// Delete object from missions bucket +async function deleteMissionObject(key: string): Promise { + try { + const command = new DeleteObjectCommand({ + Bucket: S3_CONFIG.missionsBucket, + Key: key + }); + + await s3Client.send(command); + return true; + } catch (error) { + console.error('Error deleting mission object:', error); + throw error; + } +} + // Delete mission attachment from Minio export async function deleteMissionAttachment(filePath: string): Promise { try { - await deleteObject(filePath); + await deleteMissionObject(filePath); return true; } catch (error) { console.error('Error deleting mission attachment:', error); @@ -170,7 +202,7 @@ export async function deleteMissionAttachment(filePath: string): Promise { try { - await deleteObject(filePath); + await deleteMissionObject(filePath); return true; } catch (error) { console.error('Error deleting mission logo:', error); diff --git a/lib/s3.ts b/lib/s3.ts index 88bf022e..049bad4f 100644 --- a/lib/s3.ts +++ b/lib/s3.ts @@ -20,6 +20,7 @@ export 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, + missionsBucket: process.env.MINIO_MISSIONS_BUCKET || 'missions', accessKey: process.env.MINIO_ACCESS_KEY || process.env.AWS_ACCESS_KEY_ID, secretKey: process.env.MINIO_SECRET_KEY || process.env.AWS_SECRET_ACCESS_KEY, } @@ -247,7 +248,7 @@ export async function generatePresignedUrl(key: string, expiresIn = 3600) { } // Generate a public URL for a file stored in Minio/S3 -export function getPublicUrl(filePath: string): string { +export function getPublicUrl(filePath: string, bucketName?: string): string { if (!filePath) return ''; if (filePath.startsWith('http')) return filePath; // Already a full URL @@ -269,9 +270,11 @@ export function getPublicUrl(filePath: string): string { } } + // Determine which bucket to use + const bucket = bucketName || S3_CONFIG.bucket; + // Construct the full URL using the standard approach const endpoint = S3_CONFIG.endpoint?.replace(/\/$/, ''); // Remove trailing slash if present - const bucket = S3_CONFIG.bucket; console.log('S3 Config for URL generation:', { endpoint,