missions s3

This commit is contained in:
alma 2025-05-06 13:17:44 +02:00
parent bf36e3de80
commit 6060bac5bd
5 changed files with 172 additions and 101 deletions

View File

@ -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);

View File

@ -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}`
}))
};

View File

@ -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 });
}
}

View File

@ -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({

View File

@ -187,112 +187,121 @@ export default function MissionsPage() {
</div>
) : filteredMissions.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredMissions.map((mission) => {
const oddInfo = getODDInfo(mission);
const niveauColor = getNiveauBadgeColor(mission.niveau);
return (
<div key={mission.id} className="bg-white shadow-sm hover:shadow-md transition-shadow duration-200 border border-gray-200 overflow-hidden h-full rounded-lg">
<div className="p-0">
{/* Card Header with Logo and ODD */}
<div className="flex items-start px-5 pt-5 pb-3">
<div className="flex-shrink-0 mr-4 w-16 h-16 relative">
{mission.logo ? (
<img
src={mission.logo || ''}
alt={mission.name}
className="w-full h-full object-cover rounded-md border border-gray-200"
onError={(e) => {
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}
<div
className={`logo-fallback w-full h-full flex items-center justify-center bg-gray-100 rounded-md border border-gray-200 text-gray-400 ${mission.logo ? 'hidden' : ''}`}
>
{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 (
<div key={mission.id} className="bg-white shadow-sm hover:shadow-md transition-shadow duration-200 border border-gray-200 overflow-hidden h-full rounded-lg">
<div className="p-0">
{/* Card Header with Logo and ODD */}
<div className="flex items-start px-5 pt-5 pb-3">
<div className="flex-shrink-0 mr-4 w-16 h-16 relative">
{mission.logo ? (
<img
src={mission.logo || ''}
alt={mission.name}
className="w-full h-full object-cover rounded-md border border-gray-200"
onError={(e) => {
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}
<div
className={`logo-fallback w-full h-full flex items-center justify-center bg-gray-100 rounded-md border border-gray-200 text-gray-400 ${mission.logo ? 'hidden' : ''}`}
>
{mission.name.slice(0, 2).toUpperCase()}
</div>
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
{oddInfo.number && (
<div className="flex items-center bg-gray-100 px-2 py-1 rounded-md">
<img
src={oddInfo.iconPath}
alt={oddInfo.label}
className="w-5 h-5 mr-1"
onError={(e) => {
// Fallback if image fails to load
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
<span className="text-xs font-medium text-gray-800">{oddInfo.label}</span>
</div>
)}
<span className={`text-xs font-medium px-2 py-1 rounded-md ${niveauColor}`}>
{getNiveauLabel(mission.niveau)}
</span>
</div>
<h2 className="text-base font-medium text-gray-900 line-clamp-2">{mission.name}</h2>
</div>
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
{oddInfo.number && (
<div className="flex items-center bg-gray-100 px-2 py-1 rounded-md">
<img
src={oddInfo.iconPath}
alt={oddInfo.label}
className="w-5 h-5 mr-1"
onError={(e) => {
// Fallback if image fails to load
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
<span className="text-xs font-medium text-gray-800">{oddInfo.label}</span>
{/* Card Content */}
<div className="px-5 pb-4">
<div className="flex flex-col space-y-2">
<div className="flex items-center text-sm text-gray-600">
<span className="font-medium mr-2">Type:</span>
{getMissionTypeLabel(mission.missionType)}
</div>
<div className="flex items-center text-sm text-gray-600">
<span className="font-medium mr-2">Durée:</span>
{getDuration(mission.projection)}
</div>
<div className="flex items-center text-sm text-gray-600">
<span className="font-medium mr-2">Participation:</span>
{getParticipationLabel(mission.participation)}
</div>
{mission.services && mission.services.length > 0 && (
<div className="mt-3">
<span className="text-sm font-medium text-gray-700 block mb-2">Services:</span>
<div className="flex flex-wrap gap-1.5">
{mission.services.map(service => (
<span key={service} className="bg-blue-50 text-blue-700 px-2 py-1 rounded-md text-xs font-medium">
{service}
</span>
))}
</div>
</div>
)}
<span className={`text-xs font-medium px-2 py-1 rounded-md ${niveauColor}`}>
{getNiveauLabel(mission.niveau)}
</span>
</div>
<h2 className="text-base font-medium text-gray-900 line-clamp-2">{mission.name}</h2>
</div>
</div>
{/* Card Content */}
<div className="px-5 pb-4">
<div className="flex flex-col space-y-2">
<div className="flex items-center text-sm text-gray-600">
<span className="font-medium mr-2">Type:</span>
{getMissionTypeLabel(mission.missionType)}
</div>
<div className="flex items-center text-sm text-gray-600">
<span className="font-medium mr-2">Durée:</span>
{getDuration(mission.projection)}
</div>
<div className="flex items-center text-sm text-gray-600">
<span className="font-medium mr-2">Participation:</span>
{getParticipationLabel(mission.participation)}
</div>
{mission.services && mission.services.length > 0 && (
<div className="mt-3">
<span className="text-sm font-medium text-gray-700 block mb-2">Services:</span>
<div className="flex flex-wrap gap-1.5">
{mission.services.map(service => (
<span key={service} className="bg-blue-50 text-blue-700 px-2 py-1 rounded-md text-xs font-medium">
{service}
</span>
))}
</div>
</div>
)}
{/* Card Footer */}
<div className="flex justify-between items-center px-5 py-3 border-t border-gray-100 bg-gray-50">
<span className="text-xs text-gray-500">
Créée le {formatDate(mission.createdAt)}
</span>
<Link href={`/missions/${mission.id}`}>
<Button className="bg-black hover:bg-gray-800 text-white text-xs px-3 py-1 h-7 rounded-md">
Voir détails
</Button>
</Link>
</div>
</div>
{/* Card Footer */}
<div className="flex justify-between items-center px-5 py-3 border-t border-gray-100 bg-gray-50">
<span className="text-xs text-gray-500">
Créée le {formatDate(mission.createdAt)}
</span>
<Link href={`/missions/${mission.id}`}>
<Button className="bg-black hover:bg-gray-800 text-white text-xs px-3 py-1 h-7 rounded-md">
Voir détails
</Button>
</Link>
</div>
</div>
</div>
);
})}
);
});
})()}
</div>
) : (
<div className="text-center py-16 px-6 bg-white rounded-lg border border-gray-200 shadow-sm">