Refactor image URL handling: Update API routes to return image URLs via the API instead of direct paths, ensuring consistency across avatar and background uploads. Introduce normalization functions for avatar and background URLs to maintain compatibility with existing URLs.
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m57s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m57s
This commit is contained in:
@@ -57,8 +57,8 @@ export async function POST(request: Request) {
|
|||||||
const buffer = Buffer.from(bytes);
|
const buffer = Buffer.from(bytes);
|
||||||
await writeFile(filepath, buffer);
|
await writeFile(filepath, buffer);
|
||||||
|
|
||||||
// Retourner l'URL de l'image
|
// Retourner l'URL de l'image via l'API
|
||||||
const imageUrl = `/uploads/avatars/${filename}`;
|
const imageUrl = `/api/avatars/${filename}`;
|
||||||
return NextResponse.json({ url: imageUrl });
|
return NextResponse.json({ url: imageUrl });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error uploading avatar:", error);
|
console.error("Error uploading avatar:", error);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export async function GET() {
|
|||||||
(file) =>
|
(file) =>
|
||||||
file.match(/\.(jpg|jpeg|png|gif|webp|svg)$/i) && !file.startsWith(".")
|
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 });
|
return NextResponse.json({ images });
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ export async function POST(request: Request) {
|
|||||||
const buffer = Buffer.from(bytes);
|
const buffer = Buffer.from(bytes);
|
||||||
await writeFile(filepath, buffer);
|
await writeFile(filepath, buffer);
|
||||||
|
|
||||||
// Retourner l'URL de l'image
|
// Retourner l'URL de l'image via l'API
|
||||||
const imageUrl = `/uploads/backgrounds/${filename}`;
|
const imageUrl = `/api/backgrounds/${filename}`;
|
||||||
return NextResponse.json({ url: imageUrl });
|
return NextResponse.json({ url: imageUrl });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error uploading image:", error);
|
console.error("Error uploading image:", error);
|
||||||
|
|||||||
78
app/api/avatars/[filename]/route.ts
Normal file
78
app/api/avatars/[filename]/route.ts
Normal file
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
86
app/api/backgrounds/[filename]/route.ts
Normal file
86
app/api/backgrounds/[filename]/route.ts
Normal file
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,8 +56,8 @@ export async function POST(request: Request) {
|
|||||||
const buffer = Buffer.from(bytes);
|
const buffer = Buffer.from(bytes);
|
||||||
await writeFile(filepath, buffer);
|
await writeFile(filepath, buffer);
|
||||||
|
|
||||||
// Retourner l'URL de l'image
|
// Retourner l'URL de l'image via l'API
|
||||||
const imageUrl = `/uploads/avatars/${filename}`;
|
const imageUrl = `/api/avatars/${filename}`;
|
||||||
return NextResponse.json({ url: imageUrl });
|
return NextResponse.json({ url: imageUrl });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error uploading avatar:", error);
|
console.error("Error uploading avatar:", error);
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ export async function POST(request: Request) {
|
|||||||
const buffer = Buffer.from(bytes);
|
const buffer = Buffer.from(bytes);
|
||||||
await writeFile(filepath, buffer);
|
await writeFile(filepath, buffer);
|
||||||
|
|
||||||
// Retourner l'URL de l'image
|
// Retourner l'URL de l'image via l'API
|
||||||
const imageUrl = `/uploads/avatars/${filename}`;
|
const imageUrl = `/api/avatars/${filename}`;
|
||||||
return NextResponse.json({ url: imageUrl });
|
return NextResponse.json({ url: imageUrl });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error uploading avatar:", error);
|
console.error("Error uploading avatar:", error);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { normalizeAvatarUrl } from "@/lib/avatars";
|
||||||
|
|
||||||
interface AvatarProps {
|
interface AvatarProps {
|
||||||
src: string | null | undefined;
|
src: string | null | undefined;
|
||||||
@@ -42,7 +43,8 @@ export default function Avatar({
|
|||||||
}, [src]);
|
}, [src]);
|
||||||
|
|
||||||
const sizeClass = sizeClasses[size];
|
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();
|
const initial = fallbackText || username.charAt(0).toUpperCase();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
|
import { normalizeBackgroundUrl } from "@/lib/avatars";
|
||||||
|
|
||||||
interface Preferences {
|
interface Preferences {
|
||||||
homeBackground: string | null;
|
homeBackground: string | null;
|
||||||
@@ -49,7 +50,9 @@ export function useBackgroundImage(
|
|||||||
if (preferences) {
|
if (preferences) {
|
||||||
const imageKey = `${page}Background` as keyof Preferences;
|
const imageKey = `${page}Background` as keyof Preferences;
|
||||||
const customImage = preferences[imageKey];
|
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
|
// Ne changer que si l'image est vraiment différente
|
||||||
if (targetImage !== prevImageRef.current) {
|
if (targetImage !== prevImageRef.current) {
|
||||||
|
|||||||
53
lib/avatars.ts
Normal file
53
lib/avatars.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { normalizeBackgroundUrl } from "@/lib/avatars";
|
||||||
|
|
||||||
export async function getBackgroundImage(
|
export async function getBackgroundImage(
|
||||||
page: "home" | "events" | "leaderboard",
|
page: "home" | "events" | "leaderboard",
|
||||||
@@ -16,7 +17,9 @@ export async function getBackgroundImage(
|
|||||||
const imageKey = `${page}Background` as keyof typeof sitePreferences;
|
const imageKey = `${page}Background` as keyof typeof sitePreferences;
|
||||||
const customImage = sitePreferences[imageKey];
|
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) {
|
} catch (error) {
|
||||||
console.error("Error fetching background image:", error);
|
console.error("Error fetching background image:", error);
|
||||||
return defaultImage;
|
return defaultImage;
|
||||||
|
|||||||
Reference in New Issue
Block a user