diff --git a/app/api/missions/[missionId]/attachments/route.ts b/app/api/missions/[missionId]/attachments/route.ts index 18afc045..e2eaeff1 100644 --- a/app/api/missions/[missionId]/attachments/route.ts +++ b/app/api/missions/[missionId]/attachments/route.ts @@ -64,7 +64,7 @@ export async function GET(request: Request, props: { params: Promise<{ missionId // Add public URLs to attachments const attachmentsWithUrls = attachments.map(attachment => ({ ...attachment, - publicUrl: getPublicUrl(attachment.filePath, S3_CONFIG.bucket) + publicUrl: `/api/missions/image/${attachment.filePath}` })); return NextResponse.json(attachmentsWithUrls); diff --git a/app/api/missions/[missionId]/route.ts b/app/api/missions/[missionId]/route.ts index b42630ae..1e72168d 100644 --- a/app/api/missions/[missionId]/route.ts +++ b/app/api/missions/[missionId]/route.ts @@ -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, S3_CONFIG.bucket) : null, + logoUrl: mission.logo ? `/api/missions/image/${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, S3_CONFIG.bucket) + publicUrl: `/api/missions/image/${attachment.filePath}` })) }; diff --git a/app/api/missions/image/[...path]/route.ts b/app/api/missions/image/[...path]/route.ts new file mode 100644 index 00000000..21dc71a5 --- /dev/null +++ b/app/api/missions/image/[...path]/route.ts @@ -0,0 +1,62 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/app/api/auth/options'; +import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; +import { S3_CONFIG } from '@/lib/s3'; + +// Initialize S3 client +const s3Client = new S3Client({ + region: S3_CONFIG.region, + endpoint: S3_CONFIG.endpoint, + credentials: { + accessKeyId: S3_CONFIG.accessKey || '', + secretAccessKey: S3_CONFIG.secretKey || '' + }, + forcePathStyle: true // Required for MinIO +}); + +// This endpoint serves mission images from Minio using the server's credentials +export async function GET(request: NextRequest, { params }: { params: { path: string[] } }) { + try { + // Check if path is provided + if (!params.path || params.path.length === 0) { + return new NextResponse('Path is required', { status: 400 }); + } + + // Reconstruct the full path from path segments + const filePath = params.path.join('/'); + console.log('Fetching mission image:', filePath); + + // Create S3 command to get the object + const command = new GetObjectCommand({ + Bucket: S3_CONFIG.bucket, // Use the pages bucket + Key: filePath, + }); + + // Get the object from S3/Minio + const response = await s3Client.send(command); + + if (!response.Body) { + console.error('File not found in Minio:', filePath); + return new NextResponse('File not found', { status: 404 }); + } + + // Get the readable web stream directly + const stream = response.Body.transformToWebStream(); + + // Determine content type + const contentType = response.ContentType || 'application/octet-stream'; + + // Create and return a new response with the file stream + return new NextResponse(stream as ReadableStream, { + headers: { + 'Content-Type': contentType, + 'Cache-Control': 'public, max-age=31536000', // Cache for 1 year + }, + }); + } catch (error) { + console.error('Error serving mission image:', error); + console.error('Error details:', JSON.stringify(error, null, 2)); + return new NextResponse('Internal Server Error', { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/missions/route.ts b/app/api/missions/route.ts index 0843ab13..f11a375d 100644 --- a/app/api/missions/route.ts +++ b/app/api/missions/route.ts @@ -87,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, S3_CONFIG.bucket) : null + logo: mission.logo ? `/api/missions/image/${mission.logo}` : null })); return NextResponse.json({ diff --git a/app/missions/page.tsx b/app/missions/page.tsx index 636915bd..893a26ce 100644 --- a/app/missions/page.tsx +++ b/app/missions/page.tsx @@ -187,112 +187,121 @@ export default function MissionsPage() { ) : filteredMissions.length > 0 ? (
- {filteredMissions.map((mission) => { - const oddInfo = getODDInfo(mission); - const niveauColor = getNiveauBadgeColor(mission.niveau); - - return ( -
-
- {/* Card Header with Logo and ODD */} -
-
- {mission.logo ? ( - {mission.name} { - console.log("Logo failed to load:", mission.logo); - console.log("Full URL attempted:", mission.logo); - // If the image fails to load, show the fallback - (e.currentTarget as HTMLImageElement).style.display = 'none'; - // Show the fallback div - const fallbackDiv = e.currentTarget.parentElement?.querySelector('.logo-fallback'); - if (fallbackDiv) { - (fallbackDiv as HTMLElement).style.display = 'flex'; - } - }} - /> - ) : null} -
- {mission.name.slice(0, 2).toUpperCase()} + {/* @ts-ignore */} + {(() => { + // Debug: Log all mission logos to see what URLs are being used + console.log("All mission logos:", filteredMissions.map(m => ({ + id: m.id, + name: m.name, + logo: m.logo + }))); + return filteredMissions.map((mission) => { + const oddInfo = getODDInfo(mission); + const niveauColor = getNiveauBadgeColor(mission.niveau); + + return ( +
+
+ {/* Card Header with Logo and ODD */} +
+
+ {mission.logo ? ( + {mission.name} { + console.log("Logo failed to load:", mission.logo); + console.log("Full URL attempted:", mission.logo); + // If the image fails to load, show the fallback + (e.currentTarget as HTMLImageElement).style.display = 'none'; + // Show the fallback div + const fallbackDiv = e.currentTarget.parentElement?.querySelector('.logo-fallback'); + if (fallbackDiv) { + (fallbackDiv as HTMLElement).style.display = 'flex'; + } + }} + /> + ) : null} +
+ {mission.name.slice(0, 2).toUpperCase()} +
+
+
+
+ {oddInfo.number && ( +
+ {oddInfo.label} { + // Fallback if image fails to load + (e.target as HTMLImageElement).style.display = 'none'; + }} + /> + {oddInfo.label} +
+ )} + + {getNiveauLabel(mission.niveau)} + +
+

{mission.name}

-
-
- {oddInfo.number && ( -
- {oddInfo.label} { - // Fallback if image fails to load - (e.target as HTMLImageElement).style.display = 'none'; - }} - /> - {oddInfo.label} + + {/* Card Content */} +
+
+
+ Type: + {getMissionTypeLabel(mission.missionType)} +
+ +
+ Durée: + {getDuration(mission.projection)} +
+ +
+ Participation: + {getParticipationLabel(mission.participation)} +
+ + {mission.services && mission.services.length > 0 && ( +
+ Services: +
+ {mission.services.map(service => ( + + {service} + + ))} +
)} - - {getNiveauLabel(mission.niveau)} -
-

{mission.name}

-
- - {/* Card Content */} -
-
-
- Type: - {getMissionTypeLabel(mission.missionType)} -
- -
- Durée: - {getDuration(mission.projection)} -
- -
- Participation: - {getParticipationLabel(mission.participation)} -
- - {mission.services && mission.services.length > 0 && ( -
- Services: -
- {mission.services.map(service => ( - - {service} - - ))} -
-
- )} + + {/* Card Footer */} +
+ + Créée le {formatDate(mission.createdAt)} + + + +
- - {/* Card Footer */} -
- - Créée le {formatDate(mission.createdAt)} - - - - -
-
- ); - })} + ); + }); + })()}
) : (