diff --git a/app/api/admin/avatars/upload/route.ts b/app/api/admin/avatars/upload/route.ts index 5e50558..366016c 100644 --- a/app/api/admin/avatars/upload/route.ts +++ b/app/api/admin/avatars/upload/route.ts @@ -57,8 +57,8 @@ export async function POST(request: Request) { const buffer = Buffer.from(bytes); await writeFile(filepath, buffer); - // Retourner l'URL de l'image - const imageUrl = `/uploads/avatars/${filename}`; + // Retourner l'URL de l'image via l'API + const imageUrl = `/api/avatars/${filename}`; return NextResponse.json({ url: imageUrl }); } catch (error) { console.error("Error uploading avatar:", error); diff --git a/app/api/admin/images/list/route.ts b/app/api/admin/images/list/route.ts index a660ab7..268c16a 100644 --- a/app/api/admin/images/list/route.ts +++ b/app/api/admin/images/list/route.ts @@ -26,7 +26,7 @@ export async function GET() { (file) => file.match(/\.(jpg|jpeg|png|gif|webp|svg)$/i) && !file.startsWith(".") ); - images.push(...imageFiles.map((file) => `/uploads/backgrounds/${file}`)); + images.push(...imageFiles.map((file) => `/api/backgrounds/${file}`)); } return NextResponse.json({ images }); diff --git a/app/api/admin/images/upload/route.ts b/app/api/admin/images/upload/route.ts index 776d3a8..5189b55 100644 --- a/app/api/admin/images/upload/route.ts +++ b/app/api/admin/images/upload/route.ts @@ -48,8 +48,8 @@ export async function POST(request: Request) { const buffer = Buffer.from(bytes); await writeFile(filepath, buffer); - // Retourner l'URL de l'image - const imageUrl = `/uploads/backgrounds/${filename}`; + // Retourner l'URL de l'image via l'API + const imageUrl = `/api/backgrounds/${filename}`; return NextResponse.json({ url: imageUrl }); } catch (error) { console.error("Error uploading image:", error); diff --git a/app/api/avatars/[filename]/route.ts b/app/api/avatars/[filename]/route.ts new file mode 100644 index 0000000..121bd09 --- /dev/null +++ b/app/api/avatars/[filename]/route.ts @@ -0,0 +1,78 @@ +import { NextRequest, NextResponse } from "next/server"; +import { readFile } from "fs/promises"; +import { join } from "path"; +import { existsSync } from "fs"; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ filename: string }> } +) { + try { + const { filename } = await params; + + // Sécuriser le nom de fichier pour éviter les path traversal + if ( + !filename || + filename.includes("..") || + filename.includes("/") || + filename.includes("\\") + ) { + return NextResponse.json( + { error: "Nom de fichier invalide" }, + { status: 400 } + ); + } + + // Décoder le nom de fichier (au cas où il contient des caractères encodés) + const decodedFilename = decodeURIComponent(filename); + + // Chemin vers le fichier avatar + const avatarsDir = join(process.cwd(), "public", "uploads", "avatars"); + const filepath = join(avatarsDir, decodedFilename); + + // Vérifier que le fichier existe + if (!existsSync(filepath)) { + return NextResponse.json({ error: "Avatar non trouvé" }, { status: 404 }); + } + + // Lire le fichier + const fileBuffer = await readFile(filepath); + + // Déterminer le type MIME basé sur l'extension + const extension = decodedFilename.split(".").pop()?.toLowerCase(); + let contentType = "image/jpeg"; // par défaut + + switch (extension) { + case "png": + contentType = "image/png"; + break; + case "gif": + contentType = "image/gif"; + break; + case "webp": + contentType = "image/webp"; + break; + case "svg": + contentType = "image/svg+xml"; + break; + case "jpg": + case "jpeg": + default: + contentType = "image/jpeg"; + } + + // Retourner l'image avec les bons headers + return new NextResponse(fileBuffer, { + headers: { + "Content-Type": contentType, + "Cache-Control": "public, max-age=31536000, immutable", + }, + }); + } catch (error) { + console.error("Error serving avatar:", error); + return NextResponse.json( + { error: "Erreur lors de la récupération de l'avatar" }, + { status: 500 } + ); + } +} diff --git a/app/api/backgrounds/[filename]/route.ts b/app/api/backgrounds/[filename]/route.ts new file mode 100644 index 0000000..8bf9c8f --- /dev/null +++ b/app/api/backgrounds/[filename]/route.ts @@ -0,0 +1,86 @@ +import { NextRequest, NextResponse } from "next/server"; +import { readFile } from "fs/promises"; +import { join } from "path"; +import { existsSync } from "fs"; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ filename: string }> } +) { + try { + const { filename } = await params; + + // Sécuriser le nom de fichier pour éviter les path traversal + if ( + !filename || + filename.includes("..") || + filename.includes("/") || + filename.includes("\\") + ) { + return NextResponse.json( + { error: "Nom de fichier invalide" }, + { status: 400 } + ); + } + + // Décoder le nom de fichier (au cas où il contient des caractères encodés) + const decodedFilename = decodeURIComponent(filename); + + // Chemin vers le fichier background + const backgroundsDir = join( + process.cwd(), + "public", + "uploads", + "backgrounds" + ); + const filepath = join(backgroundsDir, decodedFilename); + + // Vérifier que le fichier existe + if (!existsSync(filepath)) { + return NextResponse.json( + { error: "Image de fond non trouvée" }, + { status: 404 } + ); + } + + // Lire le fichier + const fileBuffer = await readFile(filepath); + + // Déterminer le type MIME basé sur l'extension + const extension = decodedFilename.split(".").pop()?.toLowerCase(); + let contentType = "image/jpeg"; // par défaut + + switch (extension) { + case "png": + contentType = "image/png"; + break; + case "gif": + contentType = "image/gif"; + break; + case "webp": + contentType = "image/webp"; + break; + case "svg": + contentType = "image/svg+xml"; + break; + case "jpg": + case "jpeg": + default: + contentType = "image/jpeg"; + } + + // Retourner l'image avec les bons headers + return new NextResponse(fileBuffer, { + headers: { + "Content-Type": contentType, + "Cache-Control": "public, max-age=31536000, immutable", + }, + }); + } catch (error) { + console.error("Error serving background:", error); + return NextResponse.json( + { error: "Erreur lors de la récupération de l'image de fond" }, + { status: 500 } + ); + } +} diff --git a/app/api/profile/avatar/route.ts b/app/api/profile/avatar/route.ts index f93a199..1daa85c 100644 --- a/app/api/profile/avatar/route.ts +++ b/app/api/profile/avatar/route.ts @@ -56,8 +56,8 @@ export async function POST(request: Request) { const buffer = Buffer.from(bytes); await writeFile(filepath, buffer); - // Retourner l'URL de l'image - const imageUrl = `/uploads/avatars/${filename}`; + // Retourner l'URL de l'image via l'API + const imageUrl = `/api/avatars/${filename}`; return NextResponse.json({ url: imageUrl }); } catch (error) { console.error("Error uploading avatar:", error); diff --git a/app/api/register/avatar/route.ts b/app/api/register/avatar/route.ts index f5cd247..bd2573d 100644 --- a/app/api/register/avatar/route.ts +++ b/app/api/register/avatar/route.ts @@ -50,8 +50,8 @@ export async function POST(request: Request) { const buffer = Buffer.from(bytes); await writeFile(filepath, buffer); - // Retourner l'URL de l'image - const imageUrl = `/uploads/avatars/${filename}`; + // Retourner l'URL de l'image via l'API + const imageUrl = `/api/avatars/${filename}`; return NextResponse.json({ url: imageUrl }); } catch (error) { console.error("Error uploading avatar:", error); diff --git a/components/Avatar.tsx b/components/Avatar.tsx index f39b636..5f23bfb 100644 --- a/components/Avatar.tsx +++ b/components/Avatar.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect, useRef } from "react"; +import { normalizeAvatarUrl } from "@/lib/avatars"; interface AvatarProps { src: string | null | undefined; @@ -42,7 +43,8 @@ export default function Avatar({ }, [src]); const sizeClass = sizeClasses[size]; - const displaySrc = src && !avatarError ? src : null; + const normalizedSrc = normalizeAvatarUrl(src); + const displaySrc = normalizedSrc && !avatarError ? normalizedSrc : null; const initial = fallbackText || username.charAt(0).toUpperCase(); return ( diff --git a/hooks/usePreferences.ts b/hooks/usePreferences.ts index 83c122b..a7f45d4 100644 --- a/hooks/usePreferences.ts +++ b/hooks/usePreferences.ts @@ -1,4 +1,5 @@ import { useEffect, useState, useRef } from "react"; +import { normalizeBackgroundUrl } from "@/lib/avatars"; interface Preferences { homeBackground: string | null; @@ -49,8 +50,10 @@ export function useBackgroundImage( if (preferences) { const imageKey = `${page}Background` as keyof Preferences; const customImage = preferences[imageKey]; - const targetImage = customImage || defaultImage; - + const rawImage = customImage || defaultImage; + // Normaliser l'URL pour utiliser l'API si nécessaire + const targetImage = normalizeBackgroundUrl(rawImage) || defaultImage; + // Ne changer que si l'image est vraiment différente if (targetImage !== prevImageRef.current) { prevImageRef.current = targetImage; diff --git a/lib/avatars.ts b/lib/avatars.ts new file mode 100644 index 0000000..c4491f4 --- /dev/null +++ b/lib/avatars.ts @@ -0,0 +1,53 @@ +/** + * Normalise une URL d'avatar pour utiliser la route API + * Gère la compatibilité avec les anciennes URLs directes + */ +export function normalizeAvatarUrl( + avatarUrl: string | null | undefined +): string | null { + if (!avatarUrl) { + return null; + } + + // Si c'est déjà une URL API, la retourner telle quelle + if (avatarUrl.startsWith("/api/avatars/")) { + return avatarUrl; + } + + // Si c'est une ancienne URL directe, la convertir en URL API + if (avatarUrl.startsWith("/uploads/avatars/")) { + const filename = avatarUrl.replace("/uploads/avatars/", ""); + return `/api/avatars/${filename}`; + } + + // Si c'est une URL complète (http/https) ou un chemin relatif autre, la retourner telle quelle + // (pour les avatars par défaut comme /avatar-1.jpg) + return avatarUrl; +} + +/** + * Normalise une URL d'image de fond pour utiliser la route API + * Gère la compatibilité avec les anciennes URLs directes + */ +export function normalizeBackgroundUrl( + backgroundUrl: string | null | undefined +): string | null { + if (!backgroundUrl) { + return null; + } + + // Si c'est déjà une URL API, la retourner telle quelle + if (backgroundUrl.startsWith("/api/backgrounds/")) { + return backgroundUrl; + } + + // Si c'est une ancienne URL directe, la convertir en URL API + if (backgroundUrl.startsWith("/uploads/backgrounds/")) { + const filename = backgroundUrl.replace("/uploads/backgrounds/", ""); + return `/api/backgrounds/${filename}`; + } + + // Si c'est une URL complète (http/https) ou un chemin relatif autre, la retourner telle quelle + // (pour les images par défaut comme /got-2.jpg) + return backgroundUrl; +} diff --git a/lib/preferences.ts b/lib/preferences.ts index 6db5a77..7a822ac 100644 --- a/lib/preferences.ts +++ b/lib/preferences.ts @@ -1,4 +1,5 @@ import { prisma } from "@/lib/prisma"; +import { normalizeBackgroundUrl } from "@/lib/avatars"; export async function getBackgroundImage( page: "home" | "events" | "leaderboard", @@ -16,7 +17,9 @@ export async function getBackgroundImage( const imageKey = `${page}Background` as keyof typeof sitePreferences; const customImage = sitePreferences[imageKey]; - return (customImage as string | null) || defaultImage; + const imageUrl = (customImage as string | null) || defaultImage; + // Normaliser l'URL pour utiliser l'API si nécessaire + return normalizeBackgroundUrl(imageUrl) || defaultImage; } catch (error) { console.error("Error fetching background image:", error); return defaultImage;