Compare commits

..

3 Commits

17 changed files with 317 additions and 20 deletions

View File

@@ -0,0 +1,70 @@
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { Role } from "@/prisma/generated/prisma/client";
import { writeFile, mkdir } from "fs/promises";
import { join } from "path";
import { existsSync } from "fs";
export async function POST(request: Request) {
try {
const session = await auth();
if (!session?.user || session.user.role !== Role.ADMIN) {
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
}
const formData = await request.formData();
const file = formData.get("file") as File;
if (!file) {
return NextResponse.json(
{ error: "Aucun fichier fourni" },
{ status: 400 }
);
}
// Vérifier le type de fichier
if (!file.type.startsWith("image/")) {
return NextResponse.json(
{ error: "Le fichier doit être une image" },
{ status: 400 }
);
}
// Limiter la taille (par exemple 5MB)
const maxSize = 5 * 1024 * 1024; // 5MB
if (file.size > maxSize) {
return NextResponse.json(
{ error: "L'image est trop grande (max 5MB)" },
{ status: 400 }
);
}
// Créer le dossier uploads/avatars s'il n'existe pas
const uploadsDir = join(process.cwd(), "public", "uploads");
const avatarsDir = join(uploadsDir, "avatars");
if (!existsSync(avatarsDir)) {
await mkdir(avatarsDir, { recursive: true });
}
// Générer un nom de fichier unique
const timestamp = Date.now();
const filename = `avatar-admin-${timestamp}-${file.name}`;
const filepath = join(avatarsDir, filename);
// Convertir le fichier en buffer et l'écrire
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
await writeFile(filepath, buffer);
// 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);
return NextResponse.json(
{ error: "Erreur lors de l'upload de l'avatar" },
{ status: 500 }
);
}
}

View File

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

View File

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

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

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

View File

@@ -39,24 +39,25 @@ export async function POST(request: Request) {
); );
} }
// Créer le dossier uploads s'il n'existe pas // Créer le dossier uploads/avatars s'il n'existe pas
const uploadsDir = join(process.cwd(), "public", "uploads"); const uploadsDir = join(process.cwd(), "public", "uploads");
if (!existsSync(uploadsDir)) { const avatarsDir = join(uploadsDir, "avatars");
await mkdir(uploadsDir, { recursive: true }); if (!existsSync(avatarsDir)) {
await mkdir(avatarsDir, { recursive: true });
} }
// Générer un nom de fichier unique avec l'ID utilisateur // Générer un nom de fichier unique avec l'ID utilisateur
const timestamp = Date.now(); const timestamp = Date.now();
const filename = `avatar-${session.user.id}-${timestamp}-${file.name}`; const filename = `avatar-${session.user.id}-${timestamp}-${file.name}`;
const filepath = join(uploadsDir, filename); const filepath = join(avatarsDir, filename);
// Convertir le fichier en buffer et l'écrire // Convertir le fichier en buffer et l'écrire
const bytes = await file.arrayBuffer(); const bytes = await file.arrayBuffer();
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/${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);

View File

@@ -32,25 +32,26 @@ export async function POST(request: Request) {
); );
} }
// Créer le dossier uploads s'il n'existe pas // Créer le dossier uploads/avatars s'il n'existe pas
const uploadsDir = join(process.cwd(), "public", "uploads"); const uploadsDir = join(process.cwd(), "public", "uploads");
if (!existsSync(uploadsDir)) { const avatarsDir = join(uploadsDir, "avatars");
await mkdir(uploadsDir, { recursive: true }); if (!existsSync(avatarsDir)) {
await mkdir(avatarsDir, { recursive: true });
} }
// Générer un nom de fichier unique avec timestamp // Générer un nom de fichier unique avec timestamp
const timestamp = Date.now(); const timestamp = Date.now();
const randomId = Math.random().toString(36).substring(2, 9); const randomId = Math.random().toString(36).substring(2, 9);
const filename = `avatar-register-${timestamp}-${randomId}-${file.name}`; const filename = `avatar-register-${timestamp}-${randomId}-${file.name}`;
const filepath = join(uploadsDir, filename); const filepath = join(avatarsDir, filename);
// Convertir le fichier en buffer et l'écrire // Convertir le fichier en buffer et l'écrire
const bytes = await file.arrayBuffer(); const bytes = await file.arrayBuffer();
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/${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);

View File

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

View File

@@ -349,7 +349,7 @@ export default function UserManagement() {
formData.append("file", file); formData.append("file", file);
const response = await fetch( const response = await fetch(
"/api/admin/images/upload", "/api/admin/avatars/upload",
{ {
method: "POST", method: "POST",
body: formData, body: formData,

View File

@@ -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,8 +50,10 @@ 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) {
prevImageRef.current = targetImage; prevImageRef.current = targetImage;

53
lib/avatars.ts Normal file
View 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;
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

View File

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 155 KiB