Refactor event handling and user management: Replace direct database calls with service layer methods for events, user profiles, and preferences, enhancing code organization and maintainability. Update API routes to utilize new services for event registration, feedback, and user statistics, ensuring a consistent approach across the application.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { sitePreferencesService } from "@/services/preferences/site-preferences.service";
|
||||||
import { Role } from "@/prisma/generated/prisma/client";
|
import { Role } from "@/prisma/generated/prisma/client";
|
||||||
import NavigationWrapper from "@/components/NavigationWrapper";
|
import NavigationWrapper from "@/components/NavigationWrapper";
|
||||||
import AdminPanel from "@/components/AdminPanel";
|
import AdminPanel from "@/components/AdminPanel";
|
||||||
@@ -18,22 +18,9 @@ export default async function AdminPage() {
|
|||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Récupérer les préférences globales du site
|
// Récupérer les préférences globales du site (ou créer si elles n'existent pas)
|
||||||
let sitePreferences = await prisma.sitePreferences.findUnique({
|
const sitePreferences =
|
||||||
where: { id: "global" },
|
await sitePreferencesService.getOrCreateSitePreferences();
|
||||||
});
|
|
||||||
|
|
||||||
// Si elles n'existent pas, créer une entrée par défaut
|
|
||||||
if (!sitePreferences) {
|
|
||||||
sitePreferences = await prisma.sitePreferences.create({
|
|
||||||
data: {
|
|
||||||
id: "global",
|
|
||||||
homeBackground: null,
|
|
||||||
eventsBackground: null,
|
|
||||||
leaderboardBackground: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-black relative">
|
<main className="min-h-screen bg-black relative">
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { eventService } from "@/services/events/event.service";
|
||||||
import { Role, EventType } from "@/prisma/generated/prisma/client";
|
import { Role } from "@/prisma/generated/prisma/client";
|
||||||
|
import { ValidationError, NotFoundError } from "@/services/errors";
|
||||||
|
|
||||||
export async function PUT(
|
export async function PUT(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -19,63 +20,27 @@ export async function PUT(
|
|||||||
const { date, name, description, type, room, time, maxPlaces } = body;
|
const { date, name, description, type, room, time, maxPlaces } = body;
|
||||||
// Le statut est ignoré s'il est fourni, il sera calculé automatiquement
|
// Le statut est ignoré s'il est fourni, il sera calculé automatiquement
|
||||||
|
|
||||||
// Vérifier que l'événement existe
|
const event = await eventService.validateAndUpdateEvent(id, {
|
||||||
const existingEvent = await prisma.event.findUnique({
|
date,
|
||||||
where: { id },
|
name,
|
||||||
});
|
description,
|
||||||
|
type,
|
||||||
if (!existingEvent) {
|
room,
|
||||||
return NextResponse.json(
|
time,
|
||||||
{ error: "Événement non trouvé" },
|
maxPlaces,
|
||||||
{ status: 404 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateData: {
|
|
||||||
date?: Date;
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
type?: EventType;
|
|
||||||
room?: string | null;
|
|
||||||
time?: string | null;
|
|
||||||
maxPlaces?: number | null;
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
if (date !== undefined) {
|
|
||||||
const eventDate = new Date(date);
|
|
||||||
if (isNaN(eventDate.getTime())) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Format de date invalide" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
updateData.date = eventDate;
|
|
||||||
}
|
|
||||||
if (name !== undefined) updateData.name = name;
|
|
||||||
if (description !== undefined) updateData.description = description;
|
|
||||||
if (type !== undefined) {
|
|
||||||
if (!Object.values(EventType).includes(type)) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Type d'événement invalide" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
updateData.type = type as EventType;
|
|
||||||
}
|
|
||||||
// Le statut est toujours calculé automatiquement, on ignore s'il est fourni
|
|
||||||
if (room !== undefined) updateData.room = room || null;
|
|
||||||
if (time !== undefined) updateData.time = time || null;
|
|
||||||
if (maxPlaces !== undefined)
|
|
||||||
updateData.maxPlaces = maxPlaces ? parseInt(maxPlaces) : null;
|
|
||||||
|
|
||||||
const event = await prisma.event.update({
|
|
||||||
where: { id },
|
|
||||||
data: updateData,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json(event);
|
return NextResponse.json(event);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating event:", error);
|
console.error("Error updating event:", error);
|
||||||
|
|
||||||
|
if (error instanceof ValidationError) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 400 });
|
||||||
|
}
|
||||||
|
if (error instanceof NotFoundError) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Erreur lors de la mise à jour de l'événement" },
|
{ error: "Erreur lors de la mise à jour de l'événement" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
@@ -97,9 +62,7 @@ export async function DELETE(
|
|||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
|
||||||
// Vérifier que l'événement existe
|
// Vérifier que l'événement existe
|
||||||
const existingEvent = await prisma.event.findUnique({
|
const existingEvent = await eventService.getEventById(id);
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!existingEvent) {
|
if (!existingEvent) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -108,9 +71,7 @@ export async function DELETE(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.event.delete({
|
await eventService.deleteEvent(id);
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
return NextResponse.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { eventService } from "@/services/events/event.service";
|
||||||
import { Role, EventType } from "@/prisma/generated/prisma/client";
|
import { Role } from "@/prisma/generated/prisma/client";
|
||||||
import { calculateEventStatus } from "@/lib/eventStatus";
|
import { ValidationError } from "@/services/errors";
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
@@ -12,34 +12,22 @@ export async function GET() {
|
|||||||
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const events = await prisma.event.findMany({
|
const events = await eventService.getEventsWithStatus();
|
||||||
orderBy: {
|
|
||||||
date: "desc",
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
_count: {
|
|
||||||
select: {
|
|
||||||
registrations: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Transformer les données pour inclure le nombre d'inscriptions
|
// Transformer les données pour la sérialisation
|
||||||
// Le statut est calculé automatiquement en fonction de la date
|
|
||||||
const eventsWithCount = events.map((event) => ({
|
const eventsWithCount = events.map((event) => ({
|
||||||
id: event.id,
|
id: event.id,
|
||||||
date: event.date.toISOString(),
|
date: event.date.toISOString(),
|
||||||
name: event.name,
|
name: event.name,
|
||||||
description: event.description,
|
description: event.description,
|
||||||
type: event.type,
|
type: event.type,
|
||||||
status: calculateEventStatus(event.date),
|
status: event.status,
|
||||||
room: event.room,
|
room: event.room,
|
||||||
time: event.time,
|
time: event.time,
|
||||||
maxPlaces: event.maxPlaces,
|
maxPlaces: event.maxPlaces,
|
||||||
createdAt: event.createdAt.toISOString(),
|
createdAt: event.createdAt.toISOString(),
|
||||||
updatedAt: event.updatedAt.toISOString(),
|
updatedAt: event.updatedAt.toISOString(),
|
||||||
registrationsCount: event._count.registrations,
|
registrationsCount: event.registrationsCount,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return NextResponse.json(eventsWithCount);
|
return NextResponse.json(eventsWithCount);
|
||||||
@@ -63,45 +51,24 @@ export async function POST(request: Request) {
|
|||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { date, name, description, type, room, time, maxPlaces } = body;
|
const { date, name, description, type, room, time, maxPlaces } = body;
|
||||||
|
|
||||||
if (!date || !name || !description || !type) {
|
const event = await eventService.validateAndCreateEvent({
|
||||||
return NextResponse.json(
|
date,
|
||||||
{ error: "Tous les champs sont requis" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convertir la date string en Date object
|
|
||||||
const eventDate = new Date(date);
|
|
||||||
if (isNaN(eventDate.getTime())) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Format de date invalide" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Valider les enums
|
|
||||||
if (!Object.values(EventType).includes(type)) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Type d'événement invalide" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const event = await prisma.event.create({
|
|
||||||
data: {
|
|
||||||
date: eventDate,
|
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
type: type as EventType,
|
type,
|
||||||
room: room || null,
|
room,
|
||||||
time: time || null,
|
time,
|
||||||
maxPlaces: maxPlaces ? parseInt(maxPlaces) : null,
|
maxPlaces,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json(event);
|
return NextResponse.json(event);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating event:", error);
|
console.error("Error creating event:", error);
|
||||||
|
|
||||||
|
if (error instanceof ValidationError) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Erreur lors de la création de l'événement" },
|
{ error: "Erreur lors de la création de l'événement" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { eventFeedbackService } from "@/services/events/event-feedback.service";
|
||||||
import { Role } from "@/prisma/generated/prisma/client";
|
import { Role } from "@/prisma/generated/prisma/client";
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
@@ -15,72 +15,14 @@ export async function GET() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Récupérer tous les feedbacks avec les détails de l'événement et de l'utilisateur
|
// Récupérer tous les feedbacks avec les détails de l'événement et de l'utilisateur
|
||||||
const feedbacks = await prisma.eventFeedback.findMany({
|
const feedbacks = await eventFeedbackService.getAllFeedbacks();
|
||||||
include: {
|
|
||||||
event: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
date: true,
|
|
||||||
type: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
username: true,
|
|
||||||
email: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
createdAt: "desc",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Calculer les statistiques par événement
|
// Calculer les statistiques par événement
|
||||||
const eventStats = await prisma.eventFeedback.groupBy({
|
const statistics = await eventFeedbackService.getFeedbackStatistics();
|
||||||
by: ["eventId"],
|
|
||||||
_avg: {
|
|
||||||
rating: true,
|
|
||||||
},
|
|
||||||
_count: {
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Récupérer les détails des événements pour les stats
|
|
||||||
const eventIds = eventStats.map((stat) => stat.eventId);
|
|
||||||
const events = await prisma.event.findMany({
|
|
||||||
where: {
|
|
||||||
id: {
|
|
||||||
in: eventIds,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
date: true,
|
|
||||||
type: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Combiner les stats avec les détails des événements
|
|
||||||
const statsWithDetails = eventStats.map((stat) => {
|
|
||||||
const event = events.find((e) => e.id === stat.eventId);
|
|
||||||
return {
|
|
||||||
eventId: stat.eventId,
|
|
||||||
eventName: event?.name || "Événement supprimé",
|
|
||||||
eventDate: event?.date || null,
|
|
||||||
eventType: event?.type || null,
|
|
||||||
averageRating: stat._avg.rating || 0,
|
|
||||||
feedbackCount: stat._count.id,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
feedbacks,
|
feedbacks,
|
||||||
statistics: statsWithDetails,
|
statistics,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching feedbacks:", error);
|
console.error("Error fetching feedbacks:", error);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { sitePreferencesService } from "@/services/preferences/site-preferences.service";
|
||||||
import { Role } from "@/prisma/generated/prisma/client";
|
import { Role } from "@/prisma/generated/prisma/client";
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
@@ -11,22 +11,8 @@ export async function GET() {
|
|||||||
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Récupérer les préférences globales du site
|
// Récupérer les préférences globales du site (ou créer si elles n'existent pas)
|
||||||
let sitePreferences = await prisma.sitePreferences.findUnique({
|
const sitePreferences = await sitePreferencesService.getOrCreateSitePreferences();
|
||||||
where: { id: "global" },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Si elles n'existent pas, créer une entrée par défaut
|
|
||||||
if (!sitePreferences) {
|
|
||||||
sitePreferences = await prisma.sitePreferences.create({
|
|
||||||
data: {
|
|
||||||
id: "global",
|
|
||||||
homeBackground: null,
|
|
||||||
eventsBackground: null,
|
|
||||||
leaderboardBackground: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(sitePreferences);
|
return NextResponse.json(sitePreferences);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -49,26 +35,10 @@ export async function PUT(request: Request) {
|
|||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { homeBackground, eventsBackground, leaderboardBackground } = body;
|
const { homeBackground, eventsBackground, leaderboardBackground } = body;
|
||||||
|
|
||||||
const preferences = await prisma.sitePreferences.upsert({
|
const preferences = await sitePreferencesService.updateSitePreferences({
|
||||||
where: { id: "global" },
|
homeBackground,
|
||||||
update: {
|
eventsBackground,
|
||||||
homeBackground:
|
leaderboardBackground,
|
||||||
homeBackground === "" ? null : (homeBackground ?? undefined),
|
|
||||||
eventsBackground:
|
|
||||||
eventsBackground === "" ? null : (eventsBackground ?? undefined),
|
|
||||||
leaderboardBackground:
|
|
||||||
leaderboardBackground === ""
|
|
||||||
? null
|
|
||||||
: (leaderboardBackground ?? undefined),
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: "global",
|
|
||||||
homeBackground: homeBackground === "" ? null : (homeBackground ?? null),
|
|
||||||
eventsBackground:
|
|
||||||
eventsBackground === "" ? null : (eventsBackground ?? null),
|
|
||||||
leaderboardBackground:
|
|
||||||
leaderboardBackground === "" ? null : (leaderboardBackground ?? null),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json(preferences);
|
return NextResponse.json(preferences);
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { userService } from "@/services/users/user.service";
|
||||||
|
import { userStatsService } from "@/services/users/user-stats.service";
|
||||||
import { Role } from "@/prisma/generated/prisma/client";
|
import { Role } from "@/prisma/generated/prisma/client";
|
||||||
|
import {
|
||||||
|
ValidationError,
|
||||||
|
NotFoundError,
|
||||||
|
ConflictError,
|
||||||
|
} from "@/services/errors";
|
||||||
|
|
||||||
export async function PUT(
|
export async function PUT(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -18,157 +24,26 @@ export async function PUT(
|
|||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { username, avatar, hpDelta, xpDelta, score, level, role } = body;
|
const { username, avatar, hpDelta, xpDelta, score, level, role } = body;
|
||||||
|
|
||||||
// Récupérer l'utilisateur actuel
|
// Valider username si fourni
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Utilisateur non trouvé" },
|
|
||||||
{ status: 404 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculer les nouvelles valeurs
|
|
||||||
let newHp = user.hp;
|
|
||||||
let newXp = user.xp;
|
|
||||||
let newLevel = user.level;
|
|
||||||
let newMaxXp = user.maxXp;
|
|
||||||
|
|
||||||
// Appliquer les changements de HP
|
|
||||||
if (hpDelta !== undefined) {
|
|
||||||
newHp = Math.max(0, Math.min(user.maxHp, user.hp + hpDelta));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Appliquer les changements de XP
|
|
||||||
if (xpDelta !== undefined) {
|
|
||||||
newXp = user.xp + xpDelta;
|
|
||||||
newLevel = user.level;
|
|
||||||
newMaxXp = user.maxXp;
|
|
||||||
|
|
||||||
// Gérer le niveau up si nécessaire (quand on ajoute de l'XP)
|
|
||||||
if (newXp >= newMaxXp && newXp > 0) {
|
|
||||||
while (newXp >= newMaxXp) {
|
|
||||||
newXp -= newMaxXp;
|
|
||||||
newLevel += 1;
|
|
||||||
// Augmenter le maxXp pour le prochain niveau (formule simple)
|
|
||||||
newMaxXp = Math.floor(newMaxXp * 1.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gérer le niveau down si nécessaire (quand on enlève de l'XP)
|
|
||||||
if (newXp < 0 && newLevel > 1) {
|
|
||||||
while (newXp < 0 && newLevel > 1) {
|
|
||||||
newLevel -= 1;
|
|
||||||
// Calculer le maxXp du niveau précédent
|
|
||||||
newMaxXp = Math.floor(newMaxXp / 1.2);
|
|
||||||
newXp += newMaxXp;
|
|
||||||
}
|
|
||||||
// S'assurer que l'XP ne peut pas être négative
|
|
||||||
newXp = Math.max(0, newXp);
|
|
||||||
}
|
|
||||||
|
|
||||||
// S'assurer que le niveau minimum est 1
|
|
||||||
if (newLevel < 1) {
|
|
||||||
newLevel = 1;
|
|
||||||
newXp = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Appliquer les changements directs (username, avatar, score, level, role)
|
|
||||||
const updateData: {
|
|
||||||
hp: number;
|
|
||||||
xp: number;
|
|
||||||
level: number;
|
|
||||||
maxXp: number;
|
|
||||||
username?: string;
|
|
||||||
avatar?: string | null;
|
|
||||||
score?: number;
|
|
||||||
role?: Role;
|
|
||||||
} = {
|
|
||||||
hp: newHp,
|
|
||||||
xp: newXp,
|
|
||||||
level: newLevel,
|
|
||||||
maxXp: newMaxXp,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Validation et mise à jour du username
|
|
||||||
if (username !== undefined) {
|
if (username !== undefined) {
|
||||||
if (typeof username !== "string" || username.trim().length === 0) {
|
try {
|
||||||
return NextResponse.json(
|
await userService.validateAndUpdateUserProfile(id, { username });
|
||||||
{ error: "Le nom d'utilisateur ne peut pas être vide" },
|
} catch (error) {
|
||||||
{ status: 400 }
|
if (
|
||||||
);
|
error instanceof ValidationError ||
|
||||||
|
error instanceof ConflictError
|
||||||
|
) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 400 });
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (username.length < 3 || username.length > 20) {
|
// Mettre à jour stats et profil
|
||||||
return NextResponse.json(
|
const updatedUser = await userStatsService.updateUserStatsAndProfile(
|
||||||
|
id,
|
||||||
|
{ username, avatar, hpDelta, xpDelta, score, level, role },
|
||||||
{
|
{
|
||||||
error:
|
|
||||||
"Le nom d'utilisateur doit contenir entre 3 et 20 caractères",
|
|
||||||
},
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier si le username est déjà pris par un autre utilisateur
|
|
||||||
const existingUser = await prisma.user.findFirst({
|
|
||||||
where: {
|
|
||||||
username: username.trim(),
|
|
||||||
NOT: { id },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingUser) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Ce nom d'utilisateur est déjà pris" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateData.username = username.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mise à jour de l'avatar
|
|
||||||
if (avatar !== undefined) {
|
|
||||||
updateData.avatar = avatar || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (score !== undefined) {
|
|
||||||
updateData.score = Math.max(0, score);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (level !== undefined) {
|
|
||||||
// Si le niveau est modifié directement, utiliser cette valeur
|
|
||||||
const targetLevel = Math.max(1, level);
|
|
||||||
updateData.level = targetLevel;
|
|
||||||
|
|
||||||
// Recalculer le maxXp pour le nouveau niveau
|
|
||||||
// Formule: maxXp = 5000 * (1.2 ^ (level - 1))
|
|
||||||
let calculatedMaxXp = 5000;
|
|
||||||
for (let i = 1; i < targetLevel; i++) {
|
|
||||||
calculatedMaxXp = Math.floor(calculatedMaxXp * 1.2);
|
|
||||||
}
|
|
||||||
updateData.maxXp = calculatedMaxXp;
|
|
||||||
|
|
||||||
// Réinitialiser l'XP si le niveau change directement (sauf si on modifie aussi l'XP)
|
|
||||||
if (targetLevel !== user.level && xpDelta === undefined) {
|
|
||||||
updateData.xp = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (role !== undefined) {
|
|
||||||
if (role === "ADMIN" || role === "USER") {
|
|
||||||
updateData.role = role as Role;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mettre à jour l'utilisateur
|
|
||||||
const updatedUser = await prisma.user.update({
|
|
||||||
where: { id },
|
|
||||||
data: updateData,
|
|
||||||
select: {
|
|
||||||
id: true,
|
id: true,
|
||||||
username: true,
|
username: true,
|
||||||
email: true,
|
email: true,
|
||||||
@@ -180,8 +55,8 @@ export async function PUT(
|
|||||||
xp: true,
|
xp: true,
|
||||||
maxXp: true,
|
maxXp: true,
|
||||||
avatar: true,
|
avatar: true,
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
return NextResponse.json(updatedUser);
|
return NextResponse.json(updatedUser);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -206,34 +81,19 @@ export async function DELETE(
|
|||||||
|
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
|
||||||
// Vérifier que l'utilisateur existe
|
await userService.validateAndDeleteUser(id, session.user.id);
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Utilisateur non trouvé" },
|
|
||||||
{ status: 404 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Empêcher la suppression de soi-même
|
|
||||||
if (user.id === session.user.id) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Vous ne pouvez pas supprimer votre propre compte" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Supprimer l'utilisateur (les relations seront supprimées en cascade)
|
|
||||||
await prisma.user.delete({
|
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
return NextResponse.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting user:", error);
|
console.error("Error deleting user:", error);
|
||||||
|
|
||||||
|
if (error instanceof ValidationError) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 400 });
|
||||||
|
}
|
||||||
|
if (error instanceof NotFoundError) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Erreur lors de la suppression de l'utilisateur" },
|
{ error: "Erreur lors de la suppression de l'utilisateur" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { userService } from "@/services/users/user.service";
|
||||||
import { Role } from "@/prisma/generated/prisma/client";
|
import { Role } from "@/prisma/generated/prisma/client";
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
@@ -12,7 +12,10 @@ export async function GET() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Récupérer tous les utilisateurs avec leurs stats
|
// Récupérer tous les utilisateurs avec leurs stats
|
||||||
const users = await prisma.user.findMany({
|
const users = await userService.getAllUsers({
|
||||||
|
orderBy: {
|
||||||
|
score: "desc",
|
||||||
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
username: true,
|
username: true,
|
||||||
@@ -27,9 +30,6 @@ export async function GET() {
|
|||||||
avatar: true,
|
avatar: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
},
|
},
|
||||||
orderBy: {
|
|
||||||
score: "desc",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json(users);
|
return NextResponse.json(users);
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { eventRegistrationService } from "@/services/events/event-registration.service";
|
||||||
import { calculateEventStatus } from "@/lib/eventStatus";
|
import {
|
||||||
|
ValidationError,
|
||||||
|
NotFoundError,
|
||||||
|
ConflictError,
|
||||||
|
} from "@/services/errors";
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -19,50 +23,10 @@ export async function POST(
|
|||||||
|
|
||||||
const { id: eventId } = await params;
|
const { id: eventId } = await params;
|
||||||
|
|
||||||
// Vérifier si l'événement existe
|
const registration = await eventRegistrationService.validateAndRegisterUser(
|
||||||
const event = await prisma.event.findUnique({
|
session.user.id,
|
||||||
where: { id: eventId },
|
eventId
|
||||||
});
|
|
||||||
|
|
||||||
if (!event) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Événement introuvable" },
|
|
||||||
{ status: 404 }
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
const eventStatus = calculateEventStatus(event.date);
|
|
||||||
if (eventStatus !== "UPCOMING") {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Vous ne pouvez vous inscrire qu'aux événements à venir" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier si l'utilisateur est déjà inscrit
|
|
||||||
const existingRegistration = await prisma.eventRegistration.findUnique({
|
|
||||||
where: {
|
|
||||||
userId_eventId: {
|
|
||||||
userId: session.user.id,
|
|
||||||
eventId: eventId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingRegistration) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Vous êtes déjà inscrit à cet événement" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Créer l'inscription
|
|
||||||
const registration = await prisma.eventRegistration.create({
|
|
||||||
data: {
|
|
||||||
userId: session.user.id,
|
|
||||||
eventId: eventId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ message: "Inscription réussie", registration },
|
{ message: "Inscription réussie", registration },
|
||||||
@@ -70,6 +34,17 @@ export async function POST(
|
|||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Registration error:", error);
|
console.error("Registration error:", error);
|
||||||
|
|
||||||
|
if (
|
||||||
|
error instanceof ValidationError ||
|
||||||
|
error instanceof ConflictError
|
||||||
|
) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 400 });
|
||||||
|
}
|
||||||
|
if (error instanceof NotFoundError) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Une erreur est survenue lors de l'inscription" },
|
{ error: "Une erreur est survenue lors de l'inscription" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
@@ -94,12 +69,10 @@ export async function DELETE(
|
|||||||
const { id: eventId } = await params;
|
const { id: eventId } = await params;
|
||||||
|
|
||||||
// Supprimer l'inscription
|
// Supprimer l'inscription
|
||||||
await prisma.eventRegistration.deleteMany({
|
await eventRegistrationService.unregisterUserFromEvent(
|
||||||
where: {
|
session.user.id,
|
||||||
userId: session.user.id,
|
eventId
|
||||||
eventId: eventId,
|
);
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json({ message: "Inscription annulée" });
|
return NextResponse.json({ message: "Inscription annulée" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -124,16 +97,12 @@ export async function GET(
|
|||||||
|
|
||||||
const { id: eventId } = await params;
|
const { id: eventId } = await params;
|
||||||
|
|
||||||
const registration = await prisma.eventRegistration.findUnique({
|
const isRegistered = await eventRegistrationService.checkUserRegistration(
|
||||||
where: {
|
session.user.id,
|
||||||
userId_eventId: {
|
eventId
|
||||||
userId: session.user.id,
|
);
|
||||||
eventId: eventId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json({ registered: !!registration });
|
return NextResponse.json({ registered: isRegistered });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Check registration error:", error);
|
console.error("Check registration error:", error);
|
||||||
return NextResponse.json({ registered: false });
|
return NextResponse.json({ registered: false });
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { eventService } from "@/services/events/event.service";
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -8,9 +8,7 @@ export async function GET(
|
|||||||
try {
|
try {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
|
||||||
const event = await prisma.event.findUnique({
|
const event = await eventService.getEventById(id);
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { eventService } from "@/services/events/event.service";
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
const events = await prisma.event.findMany({
|
const events = await eventService.getAllEvents({
|
||||||
orderBy: {
|
orderBy: { date: "asc" },
|
||||||
date: "asc",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json(events);
|
return NextResponse.json(events);
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { eventFeedbackService } from "@/services/events/event-feedback.service";
|
||||||
|
import {
|
||||||
|
ValidationError,
|
||||||
|
NotFoundError,
|
||||||
|
} from "@/services/errors";
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -16,49 +20,23 @@ export async function POST(
|
|||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { rating, comment } = body;
|
const { rating, comment } = body;
|
||||||
|
|
||||||
// Valider la note (1-5)
|
const feedback = await eventFeedbackService.validateAndCreateFeedback(
|
||||||
if (!rating || rating < 1 || rating > 5) {
|
session.user.id,
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "La note doit être entre 1 et 5" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier que l'événement existe
|
|
||||||
const event = await prisma.event.findUnique({
|
|
||||||
where: { id: eventId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!event) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Événement introuvable" },
|
|
||||||
{ status: 404 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Créer ou mettre à jour le feedback (unique par utilisateur/événement)
|
|
||||||
const feedback = await prisma.eventFeedback.upsert({
|
|
||||||
where: {
|
|
||||||
userId_eventId: {
|
|
||||||
userId: session.user.id,
|
|
||||||
eventId,
|
eventId,
|
||||||
},
|
{ rating, comment }
|
||||||
},
|
);
|
||||||
update: {
|
|
||||||
rating,
|
|
||||||
comment: comment || null,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
userId: session.user.id,
|
|
||||||
eventId,
|
|
||||||
rating,
|
|
||||||
comment: comment || null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json({ success: true, feedback });
|
return NextResponse.json({ success: true, feedback });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving feedback:", error);
|
console.error("Error saving feedback:", error);
|
||||||
|
|
||||||
|
if (error instanceof ValidationError) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 400 });
|
||||||
|
}
|
||||||
|
if (error instanceof NotFoundError) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Erreur lors de l'enregistrement du feedback" },
|
{ error: "Erreur lors de l'enregistrement du feedback" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
@@ -79,23 +57,10 @@ export async function GET(
|
|||||||
const { eventId } = await params;
|
const { eventId } = await params;
|
||||||
|
|
||||||
// Récupérer le feedback de l'utilisateur pour cet événement
|
// Récupérer le feedback de l'utilisateur pour cet événement
|
||||||
const feedback = await prisma.eventFeedback.findUnique({
|
const feedback = await eventFeedbackService.getUserFeedback(
|
||||||
where: {
|
session.user.id,
|
||||||
userId_eventId: {
|
eventId
|
||||||
userId: session.user.id,
|
);
|
||||||
eventId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
event: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
date: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json({ feedback });
|
return NextResponse.json({ feedback });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,49 +1,9 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { userStatsService } from "@/services/users/user-stats.service";
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
const users = await prisma.user.findMany({
|
const leaderboard = await userStatsService.getLeaderboard(10);
|
||||||
orderBy: {
|
|
||||||
score: "desc",
|
|
||||||
},
|
|
||||||
take: 10,
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
username: true,
|
|
||||||
email: true,
|
|
||||||
score: true,
|
|
||||||
level: true,
|
|
||||||
avatar: true,
|
|
||||||
bio: true,
|
|
||||||
characterClass: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const leaderboard = users.map(
|
|
||||||
(
|
|
||||||
user: {
|
|
||||||
id: string;
|
|
||||||
username: string;
|
|
||||||
email: string;
|
|
||||||
score: number;
|
|
||||||
level: number;
|
|
||||||
avatar: string | null;
|
|
||||||
bio: string | null;
|
|
||||||
characterClass: string | null;
|
|
||||||
},
|
|
||||||
index: number
|
|
||||||
) => ({
|
|
||||||
rank: index + 1,
|
|
||||||
username: user.username,
|
|
||||||
email: user.email,
|
|
||||||
score: user.score,
|
|
||||||
level: user.level,
|
|
||||||
avatar: user.avatar,
|
|
||||||
bio: user.bio,
|
|
||||||
characterClass: user.characterClass,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return NextResponse.json(leaderboard);
|
return NextResponse.json(leaderboard);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { sitePreferencesService } from "@/services/preferences/site-preferences.service";
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
// Récupérer les préférences globales du site (pas besoin d'authentification)
|
// Récupérer les préférences globales du site (pas besoin d'authentification)
|
||||||
let sitePreferences = await prisma.sitePreferences.findUnique({
|
const sitePreferences = await sitePreferencesService.getSitePreferences();
|
||||||
where: { id: "global" },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Si elles n'existent pas, retourner des valeurs par défaut
|
// Si elles n'existent pas, retourner des valeurs par défaut
|
||||||
if (!sitePreferences) {
|
if (!sitePreferences) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { userService } from "@/services/users/user.service";
|
||||||
import bcrypt from "bcryptjs";
|
import { ValidationError, NotFoundError } from "@/services/errors";
|
||||||
|
|
||||||
export async function PUT(request: Request) {
|
export async function PUT(request: Request) {
|
||||||
try {
|
try {
|
||||||
@@ -14,68 +14,24 @@ export async function PUT(request: Request) {
|
|||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { currentPassword, newPassword, confirmPassword } = body;
|
const { currentPassword, newPassword, confirmPassword } = body;
|
||||||
|
|
||||||
// Validation
|
await userService.validateAndUpdatePassword(
|
||||||
if (!currentPassword || !newPassword || !confirmPassword) {
|
session.user.id,
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Tous les champs sont requis" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newPassword.length < 6) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: "Le nouveau mot de passe doit contenir au moins 6 caractères",
|
|
||||||
},
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newPassword !== confirmPassword) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Les mots de passe ne correspondent pas" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupérer l'utilisateur avec le mot de passe
|
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: { id: session.user.id },
|
|
||||||
select: { password: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Utilisateur non trouvé" },
|
|
||||||
{ status: 404 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier l'ancien mot de passe
|
|
||||||
const isPasswordValid = await bcrypt.compare(
|
|
||||||
currentPassword,
|
currentPassword,
|
||||||
user.password
|
newPassword,
|
||||||
|
confirmPassword
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isPasswordValid) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Mot de passe actuel incorrect" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hasher le nouveau mot de passe
|
|
||||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
|
||||||
|
|
||||||
// Mettre à jour le mot de passe
|
|
||||||
await prisma.user.update({
|
|
||||||
where: { id: session.user.id },
|
|
||||||
data: { password: hashedPassword },
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json({ message: "Mot de passe modifié avec succès" });
|
return NextResponse.json({ message: "Mot de passe modifié avec succès" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating password:", error);
|
console.error("Error updating password:", error);
|
||||||
|
|
||||||
|
if (error instanceof ValidationError) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 400 });
|
||||||
|
}
|
||||||
|
if (error instanceof NotFoundError) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Erreur lors de la modification du mot de passe" },
|
{ error: "Erreur lors de la modification du mot de passe" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { userService } from "@/services/users/user.service";
|
||||||
import { CharacterClass } from "@/prisma/generated/prisma/enums";
|
import {
|
||||||
|
ValidationError,
|
||||||
|
ConflictError,
|
||||||
|
NotFoundError,
|
||||||
|
} from "@/services/errors";
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
@@ -11,9 +15,7 @@ export async function GET() {
|
|||||||
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
|
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await userService.getUserById(session.user.id, {
|
||||||
where: { id: session.user.id },
|
|
||||||
select: {
|
|
||||||
id: true,
|
id: true,
|
||||||
email: true,
|
email: true,
|
||||||
username: true,
|
username: true,
|
||||||
@@ -27,7 +29,6 @@ export async function GET() {
|
|||||||
level: true,
|
level: true,
|
||||||
score: true,
|
score: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -58,103 +59,10 @@ export async function PUT(request: Request) {
|
|||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { username, avatar, bio, characterClass } = body;
|
const { username, avatar, bio, characterClass } = body;
|
||||||
|
|
||||||
// Validation
|
const updatedUser = await userService.validateAndUpdateUserProfile(
|
||||||
if (username !== undefined) {
|
session.user.id,
|
||||||
if (typeof username !== "string" || username.trim().length === 0) {
|
{ username, avatar, bio, characterClass },
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Le nom d'utilisateur ne peut pas être vide" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (username.length < 3 || username.length > 20) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
{
|
||||||
error:
|
|
||||||
"Le nom d'utilisateur doit contenir entre 3 et 20 caractères",
|
|
||||||
},
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier si le username est déjà pris par un autre utilisateur
|
|
||||||
const existingUser = await prisma.user.findFirst({
|
|
||||||
where: {
|
|
||||||
username: username.trim(),
|
|
||||||
NOT: { id: session.user.id },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingUser) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Ce nom d'utilisateur est déjà pris" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validation bio
|
|
||||||
if (bio !== undefined && bio !== null) {
|
|
||||||
if (typeof bio !== "string") {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "La bio doit être une chaîne de caractères" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (bio.length > 500) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "La bio ne peut pas dépasser 500 caractères" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validation characterClass
|
|
||||||
const validClasses = [
|
|
||||||
"WARRIOR",
|
|
||||||
"MAGE",
|
|
||||||
"ROGUE",
|
|
||||||
"RANGER",
|
|
||||||
"PALADIN",
|
|
||||||
"ENGINEER",
|
|
||||||
"MERCHANT",
|
|
||||||
"SCHOLAR",
|
|
||||||
"BERSERKER",
|
|
||||||
"NECROMANCER",
|
|
||||||
];
|
|
||||||
if (characterClass !== undefined && characterClass !== null) {
|
|
||||||
if (!validClasses.includes(characterClass)) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Classe de personnage invalide" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mettre à jour l'utilisateur
|
|
||||||
const updateData: {
|
|
||||||
username?: string;
|
|
||||||
avatar?: string | null;
|
|
||||||
bio?: string | null;
|
|
||||||
characterClass?: CharacterClass | null;
|
|
||||||
} = {};
|
|
||||||
if (username !== undefined) {
|
|
||||||
updateData.username = username.trim();
|
|
||||||
}
|
|
||||||
if (avatar !== undefined) {
|
|
||||||
updateData.avatar = avatar || null;
|
|
||||||
}
|
|
||||||
if (bio !== undefined) {
|
|
||||||
updateData.bio = bio === null ? null : bio.trim() || null;
|
|
||||||
}
|
|
||||||
if (characterClass !== undefined) {
|
|
||||||
updateData.characterClass = (characterClass as CharacterClass) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedUser = await prisma.user.update({
|
|
||||||
where: { id: session.user.id },
|
|
||||||
data: updateData,
|
|
||||||
select: {
|
|
||||||
id: true,
|
id: true,
|
||||||
email: true,
|
email: true,
|
||||||
username: true,
|
username: true,
|
||||||
@@ -167,12 +75,17 @@ export async function PUT(request: Request) {
|
|||||||
maxXp: true,
|
maxXp: true,
|
||||||
level: true,
|
level: true,
|
||||||
score: true,
|
score: true,
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
return NextResponse.json(updatedUser);
|
return NextResponse.json(updatedUser);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating profile:", error);
|
console.error("Error updating profile:", error);
|
||||||
|
|
||||||
|
if (error instanceof ValidationError || error instanceof ConflictError) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Erreur lors de la mise à jour du profil" },
|
{ error: "Erreur lors de la mise à jour du profil" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { userService } from "@/services/users/user.service";
|
||||||
import { CharacterClass } from "@/prisma/generated/prisma/enums";
|
import {
|
||||||
|
ValidationError,
|
||||||
|
NotFoundError,
|
||||||
|
ConflictError,
|
||||||
|
} from "@/services/errors";
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
@@ -14,139 +18,10 @@ export async function POST(request: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier que l'utilisateur existe et a été créé récemment (dans les 10 dernières minutes)
|
const updatedUser = await userService.validateAndCompleteRegistration(
|
||||||
const user = await prisma.user.findUnique({
|
userId,
|
||||||
where: { id: userId },
|
{ username, avatar, bio, characterClass }
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Utilisateur non trouvé" },
|
|
||||||
{ status: 404 }
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier que le compte a été créé récemment (dans les 10 dernières minutes)
|
|
||||||
const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000);
|
|
||||||
if (user.createdAt < tenMinutesAgo) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Temps écoulé pour finaliser l'inscription" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validation username
|
|
||||||
if (username !== undefined) {
|
|
||||||
if (typeof username !== "string" || username.trim().length === 0) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Le nom d'utilisateur ne peut pas être vide" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (username.length < 3 || username.length > 20) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error:
|
|
||||||
"Le nom d'utilisateur doit contenir entre 3 et 20 caractères",
|
|
||||||
},
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier si le username est déjà pris par un autre utilisateur
|
|
||||||
const existingUser = await prisma.user.findFirst({
|
|
||||||
where: {
|
|
||||||
username: username.trim(),
|
|
||||||
NOT: { id: userId },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingUser) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Ce nom d'utilisateur est déjà pris" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validation bio
|
|
||||||
if (bio !== undefined && bio !== null) {
|
|
||||||
if (typeof bio !== "string") {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "La bio doit être une chaîne de caractères" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (bio.length > 500) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "La bio ne peut pas dépasser 500 caractères" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validation characterClass
|
|
||||||
const validClasses = [
|
|
||||||
"WARRIOR",
|
|
||||||
"MAGE",
|
|
||||||
"ROGUE",
|
|
||||||
"RANGER",
|
|
||||||
"PALADIN",
|
|
||||||
"ENGINEER",
|
|
||||||
"MERCHANT",
|
|
||||||
"SCHOLAR",
|
|
||||||
"BERSERKER",
|
|
||||||
"NECROMANCER",
|
|
||||||
];
|
|
||||||
if (characterClass !== undefined && characterClass !== null) {
|
|
||||||
if (!validClasses.includes(characterClass)) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Classe de personnage invalide" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mettre à jour l'utilisateur
|
|
||||||
const updateData: {
|
|
||||||
username?: string;
|
|
||||||
avatar?: string | null;
|
|
||||||
bio?: string | null;
|
|
||||||
characterClass?: CharacterClass | null;
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
if (username !== undefined) {
|
|
||||||
updateData.username = username.trim();
|
|
||||||
}
|
|
||||||
if (avatar !== undefined) {
|
|
||||||
updateData.avatar = avatar || null;
|
|
||||||
}
|
|
||||||
if (bio !== undefined) {
|
|
||||||
if (bio === null || bio === "") {
|
|
||||||
updateData.bio = null;
|
|
||||||
} else if (typeof bio === "string") {
|
|
||||||
updateData.bio = bio.trim() || null;
|
|
||||||
} else {
|
|
||||||
updateData.bio = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (characterClass !== undefined) {
|
|
||||||
updateData.characterClass = (characterClass as CharacterClass) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si aucun champ à mettre à jour, retourner succès quand même
|
|
||||||
if (Object.keys(updateData).length === 0) {
|
|
||||||
return NextResponse.json({
|
|
||||||
message: "Profil finalisé avec succès",
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedUser = await prisma.user.update({
|
|
||||||
where: { id: userId },
|
|
||||||
data: updateData,
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
message: "Profil finalisé avec succès",
|
message: "Profil finalisé avec succès",
|
||||||
@@ -154,11 +29,20 @@ export async function POST(request: Request) {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error completing registration:", error);
|
console.error("Error completing registration:", error);
|
||||||
const errorMessage =
|
|
||||||
error instanceof Error ? error.message : "Erreur inconnue";
|
if (
|
||||||
|
error instanceof ValidationError ||
|
||||||
|
error instanceof ConflictError
|
||||||
|
) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 400 });
|
||||||
|
}
|
||||||
|
if (error instanceof NotFoundError) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
error: `Erreur lors de la finalisation de l'inscription: ${errorMessage}`,
|
error: `Erreur lors de la finalisation de l'inscription: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
|
||||||
},
|
},
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,73 +1,19 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { userService } from "@/services/users/user.service";
|
||||||
import bcrypt from "bcryptjs";
|
import { ValidationError, ConflictError } from "@/services/errors";
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { email, username, password, bio, characterClass, avatar } = body;
|
const { email, username, password, bio, characterClass, avatar } = body;
|
||||||
|
|
||||||
if (!email || !username || !password) {
|
const user = await userService.validateAndCreateUser({
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Email, nom d'utilisateur et mot de passe sont requis" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password.length < 6) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Le mot de passe doit contenir au moins 6 caractères" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Valider characterClass si fourni
|
|
||||||
const validCharacterClasses = [
|
|
||||||
"WARRIOR",
|
|
||||||
"MAGE",
|
|
||||||
"ROGUE",
|
|
||||||
"RANGER",
|
|
||||||
"PALADIN",
|
|
||||||
"ENGINEER",
|
|
||||||
"MERCHANT",
|
|
||||||
"SCHOLAR",
|
|
||||||
"BERSERKER",
|
|
||||||
"NECROMANCER",
|
|
||||||
];
|
|
||||||
if (characterClass && !validCharacterClasses.includes(characterClass)) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Classe de personnage invalide" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier si l'email existe déjà
|
|
||||||
const existingUser = await prisma.user.findFirst({
|
|
||||||
where: {
|
|
||||||
OR: [{ email }, { username }],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingUser) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Cet email ou nom d'utilisateur est déjà utilisé" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hasher le mot de passe
|
|
||||||
const hashedPassword = await bcrypt.hash(password, 10);
|
|
||||||
|
|
||||||
// Créer l'utilisateur
|
|
||||||
const user = await prisma.user.create({
|
|
||||||
data: {
|
|
||||||
email,
|
email,
|
||||||
username,
|
username,
|
||||||
password: hashedPassword,
|
password,
|
||||||
bio: bio || null,
|
bio,
|
||||||
characterClass: characterClass || null,
|
characterClass,
|
||||||
avatar: avatar || null,
|
avatar,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -76,6 +22,11 @@ export async function POST(request: Request) {
|
|||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Registration error:", error);
|
console.error("Registration error:", error);
|
||||||
|
|
||||||
|
if (error instanceof ValidationError || error instanceof ConflictError) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Une erreur est survenue lors de l'inscription" },
|
{ error: "Une erreur est survenue lors de l'inscription" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { userService } from "@/services/users/user.service";
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -19,9 +19,7 @@ export async function GET(
|
|||||||
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await userService.getUserById(id, {
|
||||||
where: { id },
|
|
||||||
select: {
|
|
||||||
id: true,
|
id: true,
|
||||||
username: true,
|
username: true,
|
||||||
avatar: true,
|
avatar: true,
|
||||||
@@ -31,7 +29,6 @@ export async function GET(
|
|||||||
maxXp: true,
|
maxXp: true,
|
||||||
level: true,
|
level: true,
|
||||||
score: true,
|
score: true,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import NavigationWrapper from "@/components/NavigationWrapper";
|
import NavigationWrapper from "@/components/NavigationWrapper";
|
||||||
import EventsPageSection from "@/components/EventsPageSection";
|
import EventsPageSection from "@/components/EventsPageSection";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { eventService } from "@/services/events/event.service";
|
||||||
|
import { eventRegistrationService } from "@/services/events/event-registration.service";
|
||||||
import { getBackgroundImage } from "@/lib/preferences";
|
import { getBackgroundImage } from "@/lib/preferences";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export default async function EventsPage() {
|
export default async function EventsPage() {
|
||||||
const events = await prisma.event.findMany({
|
const events = await eventService.getAllEvents({
|
||||||
orderBy: {
|
orderBy: { date: "desc" },
|
||||||
date: "desc",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sérialiser les dates pour le client
|
// Sérialiser les dates pour le client
|
||||||
@@ -29,14 +28,8 @@ export default async function EventsPage() {
|
|||||||
|
|
||||||
if (session?.user?.id) {
|
if (session?.user?.id) {
|
||||||
// Récupérer toutes les inscriptions (passées et à venir) pour permettre le feedback
|
// Récupérer toutes les inscriptions (passées et à venir) pour permettre le feedback
|
||||||
const allRegistrations = await prisma.eventRegistration.findMany({
|
const allRegistrations =
|
||||||
where: {
|
await eventRegistrationService.getUserRegistrations(session.user.id);
|
||||||
userId: session.user.id,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
eventId: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
allRegistrations.forEach((reg) => {
|
allRegistrations.forEach((reg) => {
|
||||||
initialRegistrations[reg.eventId] = true;
|
initialRegistrations[reg.eventId] = true;
|
||||||
|
|||||||
@@ -1,49 +1,12 @@
|
|||||||
import NavigationWrapper from "@/components/NavigationWrapper";
|
import NavigationWrapper from "@/components/NavigationWrapper";
|
||||||
import LeaderboardSection from "@/components/LeaderboardSection";
|
import LeaderboardSection from "@/components/LeaderboardSection";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { userStatsService } from "@/services/users/user-stats.service";
|
||||||
import { getBackgroundImage } from "@/lib/preferences";
|
import { getBackgroundImage } from "@/lib/preferences";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
interface LeaderboardEntry {
|
|
||||||
rank: number;
|
|
||||||
username: string;
|
|
||||||
email: string;
|
|
||||||
score: number;
|
|
||||||
level: number;
|
|
||||||
avatar: string | null;
|
|
||||||
bio: string | null;
|
|
||||||
characterClass: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function LeaderboardPage() {
|
export default async function LeaderboardPage() {
|
||||||
const users = await prisma.user.findMany({
|
const leaderboard = await userStatsService.getLeaderboard(10);
|
||||||
orderBy: {
|
|
||||||
score: "desc",
|
|
||||||
},
|
|
||||||
take: 10,
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
username: true,
|
|
||||||
email: true,
|
|
||||||
score: true,
|
|
||||||
level: true,
|
|
||||||
avatar: true,
|
|
||||||
bio: true,
|
|
||||||
characterClass: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const leaderboard: LeaderboardEntry[] = users.map((user, index) => ({
|
|
||||||
rank: index + 1,
|
|
||||||
username: user.username,
|
|
||||||
email: user.email,
|
|
||||||
score: user.score,
|
|
||||||
level: user.level,
|
|
||||||
avatar: user.avatar,
|
|
||||||
bio: user.bio,
|
|
||||||
characterClass: user.characterClass,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const backgroundImage = await getBackgroundImage(
|
const backgroundImage = await getBackgroundImage(
|
||||||
"leaderboard",
|
"leaderboard",
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
import NavigationWrapper from "@/components/NavigationWrapper";
|
import NavigationWrapper from "@/components/NavigationWrapper";
|
||||||
import HeroSection from "@/components/HeroSection";
|
import HeroSection from "@/components/HeroSection";
|
||||||
import EventsSection from "@/components/EventsSection";
|
import EventsSection from "@/components/EventsSection";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { eventService } from "@/services/events/event.service";
|
||||||
import { getBackgroundImage } from "@/lib/preferences";
|
import { getBackgroundImage } from "@/lib/preferences";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const events = await prisma.event.findMany({
|
const events = await eventService.getUpcomingEvents(3);
|
||||||
orderBy: {
|
|
||||||
date: "asc",
|
|
||||||
},
|
|
||||||
take: 3,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Convert Date objects to strings for serialization
|
// Convert Date objects to strings for serialization
|
||||||
const serializedEvents = events.map((event) => ({
|
const serializedEvents = events.map((event) => ({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { userService } from "@/services/users/user.service";
|
||||||
import { getBackgroundImage } from "@/lib/preferences";
|
import { getBackgroundImage } from "@/lib/preferences";
|
||||||
import NavigationWrapper from "@/components/NavigationWrapper";
|
import NavigationWrapper from "@/components/NavigationWrapper";
|
||||||
import ProfileForm from "@/components/ProfileForm";
|
import ProfileForm from "@/components/ProfileForm";
|
||||||
@@ -12,9 +12,7 @@ export default async function ProfilePage() {
|
|||||||
redirect("/login");
|
redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await userService.getUserById(session.user.id, {
|
||||||
where: { id: session.user.id },
|
|
||||||
select: {
|
|
||||||
id: true,
|
id: true,
|
||||||
email: true,
|
email: true,
|
||||||
username: true,
|
username: true,
|
||||||
@@ -28,7 +26,6 @@ export default async function ProfilePage() {
|
|||||||
level: true,
|
level: true,
|
||||||
score: true,
|
score: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { userService } from "@/services/users/user.service";
|
||||||
import Navigation from "./Navigation";
|
import Navigation from "./Navigation";
|
||||||
|
|
||||||
interface UserData {
|
interface UserData {
|
||||||
@@ -19,9 +19,7 @@ export default async function NavigationWrapper() {
|
|||||||
const isAdmin = session?.user?.role === "ADMIN";
|
const isAdmin = session?.user?.role === "ADMIN";
|
||||||
|
|
||||||
if (session?.user?.id) {
|
if (session?.user?.id) {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await userService.getUserById(session.user.id, {
|
||||||
where: { id: session.user.id },
|
|
||||||
select: {
|
|
||||||
username: true,
|
username: true,
|
||||||
avatar: true,
|
avatar: true,
|
||||||
hp: true,
|
hp: true,
|
||||||
@@ -29,7 +27,6 @@ export default async function NavigationWrapper() {
|
|||||||
xp: true,
|
xp: true,
|
||||||
maxXp: true,
|
maxXp: true,
|
||||||
level: true,
|
level: true,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
|
|||||||
19
lib/auth.ts
19
lib/auth.ts
@@ -1,7 +1,6 @@
|
|||||||
import NextAuth from "next-auth";
|
import NextAuth from "next-auth";
|
||||||
import Credentials from "next-auth/providers/credentials";
|
import Credentials from "next-auth/providers/credentials";
|
||||||
import { prisma } from "./prisma";
|
import { userService } from "@/services/users/user.service";
|
||||||
import bcrypt from "bcryptjs";
|
|
||||||
import type { Role } from "@/prisma/generated/prisma/client";
|
import type { Role } from "@/prisma/generated/prisma/client";
|
||||||
|
|
||||||
export const { handlers, signIn, signOut, auth } = NextAuth({
|
export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||||
@@ -17,20 +16,12 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await userService.verifyCredentials(
|
||||||
where: { email: credentials.email as string },
|
credentials.email as string,
|
||||||
});
|
credentials.password as string
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isPasswordValid = await bcrypt.compare(
|
|
||||||
credentials.password as string,
|
|
||||||
user.password
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isPasswordValid) {
|
if (!user) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,8 @@
|
|||||||
import { prisma } from "@/lib/prisma";
|
import { sitePreferencesService } from "@/services/preferences/site-preferences.service";
|
||||||
import { normalizeBackgroundUrl } from "@/lib/avatars";
|
|
||||||
|
|
||||||
export async function getBackgroundImage(
|
export async function getBackgroundImage(
|
||||||
page: "home" | "events" | "leaderboard",
|
page: "home" | "events" | "leaderboard",
|
||||||
defaultImage: string
|
defaultImage: string
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
try {
|
return sitePreferencesService.getBackgroundImage(page, defaultImage);
|
||||||
const sitePreferences = await prisma.sitePreferences.findUnique({
|
|
||||||
where: { id: "global" },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!sitePreferences) {
|
|
||||||
return defaultImage;
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageKey = `${page}Background` as keyof typeof sitePreferences;
|
|
||||||
const customImage = sitePreferences[imageKey];
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.8 MiB |
7
services/database.ts
Normal file
7
services/database.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Database client export
|
||||||
|
* Réexport du client Prisma depuis lib/prisma.ts
|
||||||
|
* Tous les services doivent importer depuis ici, pas directement depuis lib/prisma.ts
|
||||||
|
*/
|
||||||
|
export { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
31
services/errors.ts
Normal file
31
services/errors.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Erreurs métier personnalisées
|
||||||
|
*/
|
||||||
|
export class BusinessError extends Error {
|
||||||
|
constructor(message: string, public code?: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "BusinessError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ValidationError extends BusinessError {
|
||||||
|
constructor(message: string, public field?: string) {
|
||||||
|
super(message, "VALIDATION_ERROR");
|
||||||
|
this.name = "ValidationError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotFoundError extends BusinessError {
|
||||||
|
constructor(resource: string) {
|
||||||
|
super(`${resource} non trouvé`, "NOT_FOUND");
|
||||||
|
this.name = "NotFoundError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ConflictError extends BusinessError {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message, "CONFLICT");
|
||||||
|
this.name = "ConflictError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
179
services/events/event-feedback.service.ts
Normal file
179
services/events/event-feedback.service.ts
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import { prisma } from "../database";
|
||||||
|
import type { EventFeedback, Prisma } from "@/prisma/generated/prisma/client";
|
||||||
|
import { ValidationError, NotFoundError } from "../errors";
|
||||||
|
import { eventService } from "./event.service";
|
||||||
|
|
||||||
|
export interface CreateOrUpdateFeedbackInput {
|
||||||
|
rating: number;
|
||||||
|
comment?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FeedbackStatistics {
|
||||||
|
eventId: string;
|
||||||
|
eventName: string;
|
||||||
|
eventDate: Date | null;
|
||||||
|
eventType: string | null;
|
||||||
|
averageRating: number;
|
||||||
|
feedbackCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de gestion des feedbacks sur les événements
|
||||||
|
*/
|
||||||
|
export class EventFeedbackService {
|
||||||
|
/**
|
||||||
|
* Crée ou met à jour un feedback
|
||||||
|
*/
|
||||||
|
async createOrUpdateFeedback(
|
||||||
|
userId: string,
|
||||||
|
eventId: string,
|
||||||
|
data: CreateOrUpdateFeedbackInput
|
||||||
|
): Promise<EventFeedback> {
|
||||||
|
return prisma.eventFeedback.upsert({
|
||||||
|
where: {
|
||||||
|
userId_eventId: {
|
||||||
|
userId,
|
||||||
|
eventId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
rating: data.rating,
|
||||||
|
comment: data.comment || null,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
userId,
|
||||||
|
eventId,
|
||||||
|
rating: data.rating,
|
||||||
|
comment: data.comment || null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le feedback d'un utilisateur pour un événement
|
||||||
|
*/
|
||||||
|
async getUserFeedback(
|
||||||
|
userId: string,
|
||||||
|
eventId: string
|
||||||
|
): Promise<EventFeedback | null> {
|
||||||
|
return prisma.eventFeedback.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_eventId: {
|
||||||
|
userId,
|
||||||
|
eventId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
event: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
date: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère tous les feedbacks (pour admin)
|
||||||
|
*/
|
||||||
|
async getAllFeedbacks(options?: {
|
||||||
|
include?: Prisma.EventFeedbackInclude;
|
||||||
|
orderBy?: Prisma.EventFeedbackOrderByWithRelationInput;
|
||||||
|
}): Promise<EventFeedback[]> {
|
||||||
|
return prisma.eventFeedback.findMany({
|
||||||
|
include: options?.include || {
|
||||||
|
event: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
date: true,
|
||||||
|
type: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: options?.orderBy || { createdAt: "desc" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les statistiques de feedback par événement
|
||||||
|
*/
|
||||||
|
async getFeedbackStatistics(): Promise<FeedbackStatistics[]> {
|
||||||
|
// Calculer les statistiques par événement
|
||||||
|
const eventStats = await prisma.eventFeedback.groupBy({
|
||||||
|
by: ["eventId"],
|
||||||
|
_avg: {
|
||||||
|
rating: true,
|
||||||
|
},
|
||||||
|
_count: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Récupérer les détails des événements pour les stats
|
||||||
|
const eventIds = eventStats.map((stat) => stat.eventId);
|
||||||
|
const events = await prisma.event.findMany({
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: eventIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
date: true,
|
||||||
|
type: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Combiner les stats avec les détails des événements
|
||||||
|
return eventStats.map((stat) => {
|
||||||
|
const event = events.find((e) => e.id === stat.eventId);
|
||||||
|
return {
|
||||||
|
eventId: stat.eventId,
|
||||||
|
eventName: event?.name || "Événement supprimé",
|
||||||
|
eventDate: event?.date || null,
|
||||||
|
eventType: event?.type || null,
|
||||||
|
averageRating: stat._avg.rating || 0,
|
||||||
|
feedbackCount: stat._count.id,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide et crée/met à jour un feedback avec toutes les règles métier
|
||||||
|
*/
|
||||||
|
async validateAndCreateFeedback(
|
||||||
|
userId: string,
|
||||||
|
eventId: string,
|
||||||
|
data: { rating: number; comment?: string | null }
|
||||||
|
): Promise<EventFeedback> {
|
||||||
|
// Valider la note (1-5)
|
||||||
|
if (!data.rating || data.rating < 1 || data.rating > 5) {
|
||||||
|
throw new ValidationError("La note doit être entre 1 et 5", "rating");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que l'événement existe
|
||||||
|
const event = await eventService.getEventById(eventId);
|
||||||
|
if (!event) {
|
||||||
|
throw new NotFoundError("Événement");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer ou mettre à jour le feedback
|
||||||
|
return this.createOrUpdateFeedback(userId, eventId, {
|
||||||
|
rating: data.rating,
|
||||||
|
comment: data.comment || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const eventFeedbackService = new EventFeedbackService();
|
||||||
134
services/events/event-registration.service.ts
Normal file
134
services/events/event-registration.service.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import { prisma } from "../database";
|
||||||
|
import type { EventRegistration } from "@/prisma/generated/prisma/client";
|
||||||
|
import { ValidationError, NotFoundError, ConflictError } from "../errors";
|
||||||
|
import { eventService } from "./event.service";
|
||||||
|
import { calculateEventStatus } from "@/lib/eventStatus";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de gestion des inscriptions aux événements
|
||||||
|
*/
|
||||||
|
export class EventRegistrationService {
|
||||||
|
/**
|
||||||
|
* Inscrit un utilisateur à un événement
|
||||||
|
*/
|
||||||
|
async registerUserToEvent(
|
||||||
|
userId: string,
|
||||||
|
eventId: string
|
||||||
|
): Promise<EventRegistration> {
|
||||||
|
return prisma.eventRegistration.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
eventId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Désinscrit un utilisateur d'un événement
|
||||||
|
*/
|
||||||
|
async unregisterUserFromEvent(
|
||||||
|
userId: string,
|
||||||
|
eventId: string
|
||||||
|
): Promise<void> {
|
||||||
|
await prisma.eventRegistration.deleteMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
eventId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un utilisateur est inscrit à un événement
|
||||||
|
*/
|
||||||
|
async checkUserRegistration(
|
||||||
|
userId: string,
|
||||||
|
eventId: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
const registration = await prisma.eventRegistration.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_eventId: {
|
||||||
|
userId,
|
||||||
|
eventId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return !!registration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère l'inscription d'un utilisateur à un événement
|
||||||
|
*/
|
||||||
|
async getUserRegistration(
|
||||||
|
userId: string,
|
||||||
|
eventId: string
|
||||||
|
): Promise<EventRegistration | null> {
|
||||||
|
return prisma.eventRegistration.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_eventId: {
|
||||||
|
userId,
|
||||||
|
eventId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère toutes les inscriptions d'un utilisateur
|
||||||
|
*/
|
||||||
|
async getUserRegistrations(userId: string): Promise<EventRegistration[]> {
|
||||||
|
return prisma.eventRegistration.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
eventId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le nombre d'inscriptions pour un événement
|
||||||
|
*/
|
||||||
|
async getEventRegistrationsCount(eventId: string): Promise<number> {
|
||||||
|
const count = await prisma.eventRegistration.count({
|
||||||
|
where: {
|
||||||
|
eventId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide et inscrit un utilisateur à un événement avec toutes les règles métier
|
||||||
|
*/
|
||||||
|
async validateAndRegisterUser(
|
||||||
|
userId: string,
|
||||||
|
eventId: string
|
||||||
|
): Promise<EventRegistration> {
|
||||||
|
// Vérifier que l'événement existe
|
||||||
|
const event = await eventService.getEventById(eventId);
|
||||||
|
if (!event) {
|
||||||
|
throw new NotFoundError("Événement");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que l'événement est à venir
|
||||||
|
const eventStatus = calculateEventStatus(event.date);
|
||||||
|
if (eventStatus !== "UPCOMING") {
|
||||||
|
throw new ValidationError(
|
||||||
|
"Vous ne pouvez vous inscrire qu'aux événements à venir"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si l'utilisateur est déjà inscrit
|
||||||
|
const isRegistered = await this.checkUserRegistration(userId, eventId);
|
||||||
|
if (isRegistered) {
|
||||||
|
throw new ConflictError("Vous êtes déjà inscrit à cet événement");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer l'inscription
|
||||||
|
return this.registerUserToEvent(userId, eventId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const eventRegistrationService = new EventRegistrationService();
|
||||||
278
services/events/event.service.ts
Normal file
278
services/events/event.service.ts
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
import { prisma } from "../database";
|
||||||
|
import type {
|
||||||
|
Event,
|
||||||
|
EventType,
|
||||||
|
Prisma,
|
||||||
|
} from "@/prisma/generated/prisma/client";
|
||||||
|
import { ValidationError, NotFoundError } from "../errors";
|
||||||
|
import { calculateEventStatus } from "@/lib/eventStatus";
|
||||||
|
|
||||||
|
export interface CreateEventInput {
|
||||||
|
date: Date;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
type: EventType;
|
||||||
|
room?: string | null;
|
||||||
|
time?: string | null;
|
||||||
|
maxPlaces?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateEventInput {
|
||||||
|
date?: Date;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
type?: EventType;
|
||||||
|
room?: string | null;
|
||||||
|
time?: string | null;
|
||||||
|
maxPlaces?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EventWithRegistrationsCount extends Event {
|
||||||
|
registrationsCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de gestion des événements
|
||||||
|
*/
|
||||||
|
export class EventService {
|
||||||
|
/**
|
||||||
|
* Récupère tous les événements
|
||||||
|
*/
|
||||||
|
async getAllEvents(options?: {
|
||||||
|
orderBy?: Prisma.EventOrderByWithRelationInput;
|
||||||
|
take?: number;
|
||||||
|
}): Promise<Event[]> {
|
||||||
|
return prisma.event.findMany({
|
||||||
|
orderBy: options?.orderBy || { date: "asc" },
|
||||||
|
...(options?.take && { take: options.take }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère un événement par son ID
|
||||||
|
*/
|
||||||
|
async getEventById(id: string): Promise<Event | null> {
|
||||||
|
return prisma.event.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les événements à venir
|
||||||
|
*/
|
||||||
|
async getUpcomingEvents(limit: number = 3): Promise<Event[]> {
|
||||||
|
const now = new Date();
|
||||||
|
return prisma.event.findMany({
|
||||||
|
where: {
|
||||||
|
date: {
|
||||||
|
gte: now,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
date: "asc",
|
||||||
|
},
|
||||||
|
take: limit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un nouvel événement
|
||||||
|
*/
|
||||||
|
async createEvent(data: CreateEventInput): Promise<Event> {
|
||||||
|
return prisma.event.create({
|
||||||
|
data: {
|
||||||
|
date: data.date,
|
||||||
|
name: data.name,
|
||||||
|
description: data.description,
|
||||||
|
type: data.type,
|
||||||
|
room: data.room || null,
|
||||||
|
time: data.time || null,
|
||||||
|
maxPlaces: data.maxPlaces ? parseInt(String(data.maxPlaces)) : null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour un événement
|
||||||
|
*/
|
||||||
|
async updateEvent(id: string, data: UpdateEventInput): Promise<Event> {
|
||||||
|
const updateData: Prisma.EventUpdateInput = {};
|
||||||
|
|
||||||
|
if (data.date !== undefined) {
|
||||||
|
updateData.date = data.date;
|
||||||
|
}
|
||||||
|
if (data.name !== undefined) {
|
||||||
|
updateData.name = data.name;
|
||||||
|
}
|
||||||
|
if (data.description !== undefined) {
|
||||||
|
updateData.description = data.description;
|
||||||
|
}
|
||||||
|
if (data.type !== undefined) {
|
||||||
|
updateData.type = data.type;
|
||||||
|
}
|
||||||
|
if (data.room !== undefined) {
|
||||||
|
updateData.room = data.room || null;
|
||||||
|
}
|
||||||
|
if (data.time !== undefined) {
|
||||||
|
updateData.time = data.time || null;
|
||||||
|
}
|
||||||
|
if (data.maxPlaces !== undefined) {
|
||||||
|
updateData.maxPlaces = data.maxPlaces
|
||||||
|
? parseInt(String(data.maxPlaces))
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prisma.event.update({
|
||||||
|
where: { id },
|
||||||
|
data: updateData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime un événement
|
||||||
|
*/
|
||||||
|
async deleteEvent(id: string): Promise<void> {
|
||||||
|
await prisma.event.delete({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les événements avec le nombre d'inscriptions (pour admin)
|
||||||
|
*/
|
||||||
|
async getEventsWithRegistrationsCount(): Promise<
|
||||||
|
EventWithRegistrationsCount[]
|
||||||
|
> {
|
||||||
|
const events = await prisma.event.findMany({
|
||||||
|
orderBy: {
|
||||||
|
date: "desc",
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
registrations: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return events.map((event) => ({
|
||||||
|
...event,
|
||||||
|
registrationsCount: event._count.registrations,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les événements avec leur statut calculé (pour admin)
|
||||||
|
*/
|
||||||
|
async getEventsWithStatus(): Promise<
|
||||||
|
Array<
|
||||||
|
EventWithRegistrationsCount & { status: "UPCOMING" | "LIVE" | "PAST" }
|
||||||
|
>
|
||||||
|
> {
|
||||||
|
const events = await this.getEventsWithRegistrationsCount();
|
||||||
|
return events.map((event) => ({
|
||||||
|
...event,
|
||||||
|
status: calculateEventStatus(event.date),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide et crée un événement avec toutes les règles métier
|
||||||
|
*/
|
||||||
|
async validateAndCreateEvent(data: {
|
||||||
|
date: string | Date;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
type: string;
|
||||||
|
room?: string | null;
|
||||||
|
time?: string | null;
|
||||||
|
maxPlaces?: string | number | null;
|
||||||
|
}): Promise<Event> {
|
||||||
|
// Validation des champs requis
|
||||||
|
if (!data.date || !data.name || !data.description || !data.type) {
|
||||||
|
throw new ValidationError("Tous les champs sont requis");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convertir et valider la date
|
||||||
|
const eventDate =
|
||||||
|
typeof data.date === "string" ? new Date(data.date) : data.date;
|
||||||
|
if (isNaN(eventDate.getTime())) {
|
||||||
|
throw new ValidationError("Format de date invalide", "date");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valider le type d'événement
|
||||||
|
if (!Object.values(EventType).includes(data.type as EventType)) {
|
||||||
|
throw new ValidationError("Type d'événement invalide", "type");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer l'événement
|
||||||
|
return this.createEvent({
|
||||||
|
date: eventDate,
|
||||||
|
name: data.name,
|
||||||
|
description: data.description,
|
||||||
|
type: data.type as EventType,
|
||||||
|
room: data.room || null,
|
||||||
|
time: data.time || null,
|
||||||
|
maxPlaces: data.maxPlaces ? parseInt(String(data.maxPlaces)) : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide et met à jour un événement avec toutes les règles métier
|
||||||
|
*/
|
||||||
|
async validateAndUpdateEvent(
|
||||||
|
id: string,
|
||||||
|
data: {
|
||||||
|
date?: string | Date;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
type?: string;
|
||||||
|
room?: string | null;
|
||||||
|
time?: string | null;
|
||||||
|
maxPlaces?: string | number | null;
|
||||||
|
}
|
||||||
|
): Promise<Event> {
|
||||||
|
// Vérifier que l'événement existe
|
||||||
|
const existingEvent = await this.getEventById(id);
|
||||||
|
if (!existingEvent) {
|
||||||
|
throw new NotFoundError("Événement");
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData: UpdateEventInput = {};
|
||||||
|
|
||||||
|
// Valider et convertir la date si fournie
|
||||||
|
if (data.date !== undefined) {
|
||||||
|
const eventDate =
|
||||||
|
typeof data.date === "string" ? new Date(data.date) : data.date;
|
||||||
|
if (isNaN(eventDate.getTime())) {
|
||||||
|
throw new ValidationError("Format de date invalide", "date");
|
||||||
|
}
|
||||||
|
updateData.date = eventDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valider le type si fourni
|
||||||
|
if (data.type !== undefined) {
|
||||||
|
if (!Object.values(EventType).includes(data.type as EventType)) {
|
||||||
|
throw new ValidationError("Type d'événement invalide", "type");
|
||||||
|
}
|
||||||
|
updateData.type = data.type as EventType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Autres champs
|
||||||
|
if (data.name !== undefined) updateData.name = data.name;
|
||||||
|
if (data.description !== undefined)
|
||||||
|
updateData.description = data.description;
|
||||||
|
if (data.room !== undefined) updateData.room = data.room || null;
|
||||||
|
if (data.time !== undefined) updateData.time = data.time || null;
|
||||||
|
if (data.maxPlaces !== undefined) {
|
||||||
|
updateData.maxPlaces = data.maxPlaces
|
||||||
|
? parseInt(String(data.maxPlaces))
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.updateEvent(id, updateData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const eventService = new EventService();
|
||||||
111
services/preferences/site-preferences.service.ts
Normal file
111
services/preferences/site-preferences.service.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { prisma } from "../database";
|
||||||
|
import { normalizeBackgroundUrl } from "@/lib/avatars";
|
||||||
|
import type { SitePreferences } from "@/prisma/generated/prisma/client";
|
||||||
|
|
||||||
|
export interface UpdateSitePreferencesInput {
|
||||||
|
homeBackground?: string | null;
|
||||||
|
eventsBackground?: string | null;
|
||||||
|
leaderboardBackground?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de gestion des préférences globales du site
|
||||||
|
*/
|
||||||
|
export class SitePreferencesService {
|
||||||
|
/**
|
||||||
|
* Récupère les préférences du site
|
||||||
|
*/
|
||||||
|
async getSitePreferences(): Promise<SitePreferences | null> {
|
||||||
|
return prisma.sitePreferences.findUnique({
|
||||||
|
where: { id: "global" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les préférences du site ou crée une entrée par défaut
|
||||||
|
*/
|
||||||
|
async getOrCreateSitePreferences(): Promise<SitePreferences> {
|
||||||
|
let sitePreferences = await prisma.sitePreferences.findUnique({
|
||||||
|
where: { id: "global" },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!sitePreferences) {
|
||||||
|
sitePreferences = await prisma.sitePreferences.create({
|
||||||
|
data: {
|
||||||
|
id: "global",
|
||||||
|
homeBackground: null,
|
||||||
|
eventsBackground: null,
|
||||||
|
leaderboardBackground: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return sitePreferences;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour les préférences du site
|
||||||
|
*/
|
||||||
|
async updateSitePreferences(
|
||||||
|
data: UpdateSitePreferencesInput
|
||||||
|
): Promise<SitePreferences> {
|
||||||
|
return prisma.sitePreferences.upsert({
|
||||||
|
where: { id: "global" },
|
||||||
|
update: {
|
||||||
|
homeBackground:
|
||||||
|
data.homeBackground === ""
|
||||||
|
? null
|
||||||
|
: (data.homeBackground ?? undefined),
|
||||||
|
eventsBackground:
|
||||||
|
data.eventsBackground === ""
|
||||||
|
? null
|
||||||
|
: (data.eventsBackground ?? undefined),
|
||||||
|
leaderboardBackground:
|
||||||
|
data.leaderboardBackground === ""
|
||||||
|
? null
|
||||||
|
: (data.leaderboardBackground ?? undefined),
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: "global",
|
||||||
|
homeBackground:
|
||||||
|
data.homeBackground === "" ? null : (data.homeBackground ?? null),
|
||||||
|
eventsBackground:
|
||||||
|
data.eventsBackground === "" ? null : (data.eventsBackground ?? null),
|
||||||
|
leaderboardBackground:
|
||||||
|
data.leaderboardBackground === ""
|
||||||
|
? null
|
||||||
|
: (data.leaderboardBackground ?? null),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère l'image de fond pour une page donnée
|
||||||
|
*/
|
||||||
|
async getBackgroundImage(
|
||||||
|
page: "home" | "events" | "leaderboard",
|
||||||
|
defaultImage: string
|
||||||
|
): Promise<string> {
|
||||||
|
try {
|
||||||
|
const sitePreferences = await prisma.sitePreferences.findUnique({
|
||||||
|
where: { id: "global" },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!sitePreferences) {
|
||||||
|
return defaultImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageKey = `${page}Background` as keyof typeof sitePreferences;
|
||||||
|
const customImage = sitePreferences[imageKey];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sitePreferencesService = new SitePreferencesService();
|
||||||
251
services/users/user-stats.service.ts
Normal file
251
services/users/user-stats.service.ts
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
import { prisma } from "../database";
|
||||||
|
import type { User, Role, Prisma } from "@/prisma/generated/prisma/client";
|
||||||
|
import { NotFoundError } from "../errors";
|
||||||
|
import { userService } from "./user.service";
|
||||||
|
|
||||||
|
export interface UpdateUserStatsInput {
|
||||||
|
hpDelta?: number;
|
||||||
|
xpDelta?: number;
|
||||||
|
score?: number;
|
||||||
|
level?: number;
|
||||||
|
role?: Role;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeaderboardEntry {
|
||||||
|
rank: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
score: number;
|
||||||
|
level: number;
|
||||||
|
avatar: string | null;
|
||||||
|
bio: string | null;
|
||||||
|
characterClass: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de gestion des statistiques utilisateur
|
||||||
|
*/
|
||||||
|
export class UserStatsService {
|
||||||
|
/**
|
||||||
|
* Récupère le leaderboard
|
||||||
|
*/
|
||||||
|
async getLeaderboard(limit: number = 10): Promise<LeaderboardEntry[]> {
|
||||||
|
const users = await prisma.user.findMany({
|
||||||
|
orderBy: {
|
||||||
|
score: "desc",
|
||||||
|
},
|
||||||
|
take: limit,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
email: true,
|
||||||
|
score: true,
|
||||||
|
level: true,
|
||||||
|
avatar: true,
|
||||||
|
bio: true,
|
||||||
|
characterClass: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return users.map((user, index) => ({
|
||||||
|
rank: index + 1,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
score: user.score,
|
||||||
|
level: user.level,
|
||||||
|
avatar: user.avatar,
|
||||||
|
bio: user.bio,
|
||||||
|
characterClass: user.characterClass,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour les statistiques d'un utilisateur
|
||||||
|
*/
|
||||||
|
async updateUserStats(
|
||||||
|
id: string,
|
||||||
|
stats: UpdateUserStatsInput,
|
||||||
|
select?: Prisma.UserSelect
|
||||||
|
): Promise<User> {
|
||||||
|
// Récupérer l'utilisateur actuel
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("Utilisateur non trouvé");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculer les nouvelles valeurs
|
||||||
|
let newHp = user.hp;
|
||||||
|
let newXp = user.xp;
|
||||||
|
let newLevel = user.level;
|
||||||
|
let newMaxXp = user.maxXp;
|
||||||
|
|
||||||
|
// Appliquer les changements de HP
|
||||||
|
if (stats.hpDelta !== undefined) {
|
||||||
|
newHp = Math.max(0, Math.min(user.maxHp, user.hp + stats.hpDelta));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Appliquer les changements de XP
|
||||||
|
if (stats.xpDelta !== undefined) {
|
||||||
|
newXp = user.xp + stats.xpDelta;
|
||||||
|
newLevel = user.level;
|
||||||
|
newMaxXp = user.maxXp;
|
||||||
|
|
||||||
|
// Gérer le niveau up si nécessaire (quand on ajoute de l'XP)
|
||||||
|
if (newXp >= newMaxXp && newXp > 0) {
|
||||||
|
while (newXp >= newMaxXp) {
|
||||||
|
newXp -= newMaxXp;
|
||||||
|
newLevel += 1;
|
||||||
|
// Augmenter le maxXp pour le prochain niveau (formule simple)
|
||||||
|
newMaxXp = Math.floor(newMaxXp * 1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gérer le niveau down si nécessaire (quand on enlève de l'XP)
|
||||||
|
if (newXp < 0 && newLevel > 1) {
|
||||||
|
while (newXp < 0 && newLevel > 1) {
|
||||||
|
newLevel -= 1;
|
||||||
|
// Calculer le maxXp du niveau précédent
|
||||||
|
newMaxXp = Math.floor(newMaxXp / 1.2);
|
||||||
|
newXp += newMaxXp;
|
||||||
|
}
|
||||||
|
// S'assurer que l'XP ne peut pas être négative
|
||||||
|
newXp = Math.max(0, newXp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// S'assurer que le niveau minimum est 1
|
||||||
|
if (newLevel < 1) {
|
||||||
|
newLevel = 1;
|
||||||
|
newXp = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construire les données de mise à jour
|
||||||
|
const updateData: Prisma.UserUpdateInput = {
|
||||||
|
hp: newHp,
|
||||||
|
xp: newXp,
|
||||||
|
level: newLevel,
|
||||||
|
maxXp: newMaxXp,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Appliquer les changements directs
|
||||||
|
if (stats.score !== undefined) {
|
||||||
|
updateData.score = Math.max(0, stats.score);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.level !== undefined) {
|
||||||
|
// Si le niveau est modifié directement, utiliser cette valeur
|
||||||
|
const targetLevel = Math.max(1, stats.level);
|
||||||
|
updateData.level = targetLevel;
|
||||||
|
|
||||||
|
// Recalculer le maxXp pour le nouveau niveau
|
||||||
|
// Formule: maxXp = 5000 * (1.2 ^ (level - 1))
|
||||||
|
let calculatedMaxXp = 5000;
|
||||||
|
for (let i = 1; i < targetLevel; i++) {
|
||||||
|
calculatedMaxXp = Math.floor(calculatedMaxXp * 1.2);
|
||||||
|
}
|
||||||
|
updateData.maxXp = calculatedMaxXp;
|
||||||
|
|
||||||
|
// Réinitialiser l'XP si le niveau change directement (sauf si on modifie aussi l'XP)
|
||||||
|
if (targetLevel !== user.level && stats.xpDelta === undefined) {
|
||||||
|
updateData.xp = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.role !== undefined) {
|
||||||
|
if (stats.role === "ADMIN" || stats.role === "USER") {
|
||||||
|
updateData.role = stats.role;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return prisma.user.update({
|
||||||
|
where: { id },
|
||||||
|
data: updateData,
|
||||||
|
select,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour les stats ET le profil d'un utilisateur (pour admin)
|
||||||
|
*/
|
||||||
|
async updateUserStatsAndProfile(
|
||||||
|
id: string,
|
||||||
|
data: {
|
||||||
|
username?: string;
|
||||||
|
avatar?: string | null;
|
||||||
|
hpDelta?: number;
|
||||||
|
xpDelta?: number;
|
||||||
|
score?: number;
|
||||||
|
level?: number;
|
||||||
|
role?: Role;
|
||||||
|
},
|
||||||
|
select?: Prisma.UserSelect
|
||||||
|
): Promise<User> {
|
||||||
|
// Vérifier que l'utilisateur existe
|
||||||
|
const user = await userService.getUserById(id);
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundError("Utilisateur");
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectFields = select || {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
email: true,
|
||||||
|
role: true,
|
||||||
|
score: true,
|
||||||
|
level: true,
|
||||||
|
hp: true,
|
||||||
|
maxHp: true,
|
||||||
|
xp: true,
|
||||||
|
maxXp: true,
|
||||||
|
avatar: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mettre à jour les stats si nécessaire
|
||||||
|
const hasStatsChanges =
|
||||||
|
data.hpDelta !== undefined ||
|
||||||
|
data.xpDelta !== undefined ||
|
||||||
|
data.score !== undefined ||
|
||||||
|
data.level !== undefined ||
|
||||||
|
data.role !== undefined;
|
||||||
|
|
||||||
|
let updatedUser: User;
|
||||||
|
if (hasStatsChanges) {
|
||||||
|
updatedUser = await this.updateUserStats(
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
hpDelta: data.hpDelta,
|
||||||
|
xpDelta: data.xpDelta,
|
||||||
|
score: data.score,
|
||||||
|
level: data.level,
|
||||||
|
role: data.role,
|
||||||
|
},
|
||||||
|
selectFields
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
updatedUser = await userService.getUserById(id, selectFields);
|
||||||
|
if (!updatedUser) {
|
||||||
|
throw new NotFoundError("Utilisateur");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour username/avatar si nécessaire
|
||||||
|
if (data.username !== undefined || data.avatar !== undefined) {
|
||||||
|
updatedUser = await userService.updateUser(
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
username:
|
||||||
|
data.username !== undefined ? data.username.trim() : undefined,
|
||||||
|
avatar: data.avatar,
|
||||||
|
},
|
||||||
|
selectFields
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userStatsService = new UserStatsService();
|
||||||
515
services/users/user.service.ts
Normal file
515
services/users/user.service.ts
Normal file
@@ -0,0 +1,515 @@
|
|||||||
|
import { prisma } from "../database";
|
||||||
|
import bcrypt from "bcryptjs";
|
||||||
|
import type {
|
||||||
|
User,
|
||||||
|
CharacterClass,
|
||||||
|
Role,
|
||||||
|
Prisma,
|
||||||
|
} from "@/prisma/generated/prisma/client";
|
||||||
|
import { ValidationError, NotFoundError, ConflictError } from "../errors";
|
||||||
|
|
||||||
|
// Constantes de validation
|
||||||
|
const VALID_CHARACTER_CLASSES = [
|
||||||
|
"WARRIOR",
|
||||||
|
"MAGE",
|
||||||
|
"ROGUE",
|
||||||
|
"RANGER",
|
||||||
|
"PALADIN",
|
||||||
|
"ENGINEER",
|
||||||
|
"MERCHANT",
|
||||||
|
"SCHOLAR",
|
||||||
|
"BERSERKER",
|
||||||
|
"NECROMANCER",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const USERNAME_MIN_LENGTH = 3;
|
||||||
|
const USERNAME_MAX_LENGTH = 20;
|
||||||
|
const BIO_MAX_LENGTH = 500;
|
||||||
|
const PASSWORD_MIN_LENGTH = 6;
|
||||||
|
|
||||||
|
export interface CreateUserInput {
|
||||||
|
email: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
bio?: string | null;
|
||||||
|
characterClass?: CharacterClass | null;
|
||||||
|
avatar?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateUserInput {
|
||||||
|
username?: string;
|
||||||
|
avatar?: string | null;
|
||||||
|
bio?: string | null;
|
||||||
|
characterClass?: CharacterClass | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserSelect {
|
||||||
|
id?: boolean;
|
||||||
|
email?: boolean;
|
||||||
|
username?: boolean;
|
||||||
|
avatar?: boolean;
|
||||||
|
bio?: boolean;
|
||||||
|
characterClass?: boolean;
|
||||||
|
hp?: boolean;
|
||||||
|
maxHp?: boolean;
|
||||||
|
xp?: boolean;
|
||||||
|
maxXp?: boolean;
|
||||||
|
level?: boolean;
|
||||||
|
score?: boolean;
|
||||||
|
role?: boolean;
|
||||||
|
createdAt?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de gestion des utilisateurs
|
||||||
|
*/
|
||||||
|
export class UserService {
|
||||||
|
/**
|
||||||
|
* Récupère un utilisateur par son ID
|
||||||
|
*/
|
||||||
|
async getUserById(
|
||||||
|
id: string,
|
||||||
|
select?: Prisma.UserSelect
|
||||||
|
): Promise<User | null> {
|
||||||
|
return prisma.user.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère un utilisateur par son email
|
||||||
|
*/
|
||||||
|
async getUserByEmail(email: string): Promise<User | null> {
|
||||||
|
return prisma.user.findUnique({
|
||||||
|
where: { email },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère un utilisateur par son username
|
||||||
|
*/
|
||||||
|
async getUserByUsername(username: string): Promise<User | null> {
|
||||||
|
return prisma.user.findUnique({
|
||||||
|
where: { username },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un username est disponible
|
||||||
|
*/
|
||||||
|
async checkUsernameAvailability(
|
||||||
|
username: string,
|
||||||
|
excludeUserId?: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
const existingUser = await prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
username: username.trim(),
|
||||||
|
...(excludeUserId && { NOT: { id: excludeUserId } }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return !existingUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un email ou username existe déjà
|
||||||
|
*/
|
||||||
|
async checkEmailOrUsernameExists(
|
||||||
|
email: string,
|
||||||
|
username: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
const existingUser = await prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
OR: [{ email }, { username }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return !!existingUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un nouvel utilisateur
|
||||||
|
*/
|
||||||
|
async createUser(data: CreateUserInput): Promise<User> {
|
||||||
|
// Hasher le mot de passe
|
||||||
|
const hashedPassword = await bcrypt.hash(data.password, 10);
|
||||||
|
|
||||||
|
return prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: data.email,
|
||||||
|
username: data.username,
|
||||||
|
password: hashedPassword,
|
||||||
|
bio: data.bio || null,
|
||||||
|
characterClass: data.characterClass || null,
|
||||||
|
avatar: data.avatar || null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour un utilisateur
|
||||||
|
*/
|
||||||
|
async updateUser(
|
||||||
|
id: string,
|
||||||
|
data: UpdateUserInput,
|
||||||
|
select?: Prisma.UserSelect
|
||||||
|
): Promise<User> {
|
||||||
|
const updateData: Prisma.UserUpdateInput = {};
|
||||||
|
|
||||||
|
if (data.username !== undefined) {
|
||||||
|
updateData.username = data.username.trim();
|
||||||
|
}
|
||||||
|
if (data.avatar !== undefined) {
|
||||||
|
updateData.avatar = data.avatar || null;
|
||||||
|
}
|
||||||
|
if (data.bio !== undefined) {
|
||||||
|
updateData.bio = data.bio === null ? null : data.bio.trim() || null;
|
||||||
|
}
|
||||||
|
if (data.characterClass !== undefined) {
|
||||||
|
updateData.characterClass = data.characterClass || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prisma.user.update({
|
||||||
|
where: { id },
|
||||||
|
data: updateData,
|
||||||
|
select,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour le mot de passe d'un utilisateur
|
||||||
|
*/
|
||||||
|
async updateUserPassword(
|
||||||
|
id: string,
|
||||||
|
currentPassword: string,
|
||||||
|
newPassword: string
|
||||||
|
): Promise<void> {
|
||||||
|
// Récupérer l'utilisateur avec le mot de passe
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: { password: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("Utilisateur non trouvé");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier l'ancien mot de passe
|
||||||
|
const isPasswordValid = await bcrypt.compare(
|
||||||
|
currentPassword,
|
||||||
|
user.password
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isPasswordValid) {
|
||||||
|
throw new Error("Mot de passe actuel incorrect");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hasher le nouveau mot de passe
|
||||||
|
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||||
|
|
||||||
|
// Mettre à jour le mot de passe
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id },
|
||||||
|
data: { password: hashedPassword },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime un utilisateur
|
||||||
|
*/
|
||||||
|
async deleteUser(id: string): Promise<void> {
|
||||||
|
await prisma.user.delete({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère tous les utilisateurs (pour admin)
|
||||||
|
*/
|
||||||
|
async getAllUsers(options?: {
|
||||||
|
orderBy?: Prisma.UserOrderByWithRelationInput;
|
||||||
|
select?: Prisma.UserSelect;
|
||||||
|
}): Promise<User[]> {
|
||||||
|
return prisma.user.findMany({
|
||||||
|
orderBy: options?.orderBy || { score: "desc" },
|
||||||
|
select: options?.select,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie les credentials pour l'authentification
|
||||||
|
*/
|
||||||
|
async verifyCredentials(
|
||||||
|
email: string,
|
||||||
|
password: string
|
||||||
|
): Promise<User | null> {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { email },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPasswordValid = await bcrypt.compare(password, user.password);
|
||||||
|
|
||||||
|
if (!isPasswordValid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un compte a été créé récemment (dans les X minutes)
|
||||||
|
*/
|
||||||
|
async isAccountRecentlyCreated(
|
||||||
|
userId: string,
|
||||||
|
minutesAgo: number = 10
|
||||||
|
): Promise<boolean> {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
select: { createdAt: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeLimit = new Date(Date.now() - minutesAgo * 60 * 1000);
|
||||||
|
return user.createdAt >= timeLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide et crée un utilisateur avec toutes les règles métier
|
||||||
|
*/
|
||||||
|
async validateAndCreateUser(data: {
|
||||||
|
email: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
bio?: string | null;
|
||||||
|
characterClass?: string | null;
|
||||||
|
avatar?: string | null;
|
||||||
|
}): Promise<User> {
|
||||||
|
// Validation des champs requis
|
||||||
|
if (!data.email || !data.username || !data.password) {
|
||||||
|
throw new ValidationError(
|
||||||
|
"Email, nom d'utilisateur et mot de passe sont requis"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation du mot de passe
|
||||||
|
if (data.password.length < PASSWORD_MIN_LENGTH) {
|
||||||
|
throw new ValidationError(
|
||||||
|
`Le mot de passe doit contenir au moins ${PASSWORD_MIN_LENGTH} caractères`,
|
||||||
|
"password"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation du characterClass
|
||||||
|
if (
|
||||||
|
data.characterClass &&
|
||||||
|
!VALID_CHARACTER_CLASSES.includes(data.characterClass as CharacterClass)
|
||||||
|
) {
|
||||||
|
throw new ValidationError(
|
||||||
|
"Classe de personnage invalide",
|
||||||
|
"characterClass"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si l'email ou username existe déjà
|
||||||
|
const exists = await this.checkEmailOrUsernameExists(
|
||||||
|
data.email,
|
||||||
|
data.username
|
||||||
|
);
|
||||||
|
if (exists) {
|
||||||
|
throw new ConflictError(
|
||||||
|
"Cet email ou nom d'utilisateur est déjà utilisé"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer l'utilisateur
|
||||||
|
return this.createUser({
|
||||||
|
email: data.email,
|
||||||
|
username: data.username,
|
||||||
|
password: data.password,
|
||||||
|
bio: data.bio || null,
|
||||||
|
characterClass: (data.characterClass as CharacterClass) || null,
|
||||||
|
avatar: data.avatar || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide et met à jour le profil utilisateur avec toutes les règles métier
|
||||||
|
*/
|
||||||
|
async validateAndUpdateUserProfile(
|
||||||
|
userId: string,
|
||||||
|
data: {
|
||||||
|
username?: string;
|
||||||
|
avatar?: string | null;
|
||||||
|
bio?: string | null;
|
||||||
|
characterClass?: string | null;
|
||||||
|
},
|
||||||
|
select?: Prisma.UserSelect
|
||||||
|
): Promise<User> {
|
||||||
|
// Validation username
|
||||||
|
if (data.username !== undefined) {
|
||||||
|
if (
|
||||||
|
typeof data.username !== "string" ||
|
||||||
|
data.username.trim().length === 0
|
||||||
|
) {
|
||||||
|
throw new ValidationError(
|
||||||
|
"Le nom d'utilisateur ne peut pas être vide",
|
||||||
|
"username"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
data.username.length < USERNAME_MIN_LENGTH ||
|
||||||
|
data.username.length > USERNAME_MAX_LENGTH
|
||||||
|
) {
|
||||||
|
throw new ValidationError(
|
||||||
|
`Le nom d'utilisateur doit contenir entre ${USERNAME_MIN_LENGTH} et ${USERNAME_MAX_LENGTH} caractères`,
|
||||||
|
"username"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si le username est déjà pris
|
||||||
|
const isAvailable = await this.checkUsernameAvailability(
|
||||||
|
data.username.trim(),
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
if (!isAvailable) {
|
||||||
|
throw new ConflictError("Ce nom d'utilisateur est déjà pris");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation bio
|
||||||
|
if (data.bio !== undefined && data.bio !== null) {
|
||||||
|
if (typeof data.bio !== "string") {
|
||||||
|
throw new ValidationError(
|
||||||
|
"La bio doit être une chaîne de caractères",
|
||||||
|
"bio"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (data.bio.length > BIO_MAX_LENGTH) {
|
||||||
|
throw new ValidationError(
|
||||||
|
`La bio ne peut pas dépasser ${BIO_MAX_LENGTH} caractères`,
|
||||||
|
"bio"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation characterClass
|
||||||
|
if (
|
||||||
|
data.characterClass !== undefined &&
|
||||||
|
data.characterClass !== null &&
|
||||||
|
!VALID_CHARACTER_CLASSES.includes(data.characterClass as CharacterClass)
|
||||||
|
) {
|
||||||
|
throw new ValidationError(
|
||||||
|
"Classe de personnage invalide",
|
||||||
|
"characterClass"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour l'utilisateur
|
||||||
|
return this.updateUser(
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
username:
|
||||||
|
data.username !== undefined ? data.username.trim() : undefined,
|
||||||
|
avatar: data.avatar,
|
||||||
|
bio: data.bio,
|
||||||
|
characterClass: data.characterClass as CharacterClass | null,
|
||||||
|
},
|
||||||
|
select
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide et finalise l'inscription d'un utilisateur (avec vérification du temps)
|
||||||
|
*/
|
||||||
|
async validateAndCompleteRegistration(
|
||||||
|
userId: string,
|
||||||
|
data: {
|
||||||
|
username?: string;
|
||||||
|
avatar?: string | null;
|
||||||
|
bio?: string | null;
|
||||||
|
characterClass?: string | null;
|
||||||
|
}
|
||||||
|
): Promise<User> {
|
||||||
|
// Vérifier que l'utilisateur existe
|
||||||
|
const user = await this.getUserById(userId);
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundError("Utilisateur");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que le compte a été créé récemment (dans les 10 dernières minutes)
|
||||||
|
const isRecent = await this.isAccountRecentlyCreated(userId, 10);
|
||||||
|
if (!isRecent) {
|
||||||
|
throw new ValidationError("Temps écoulé pour finaliser l'inscription");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valider et mettre à jour
|
||||||
|
return this.validateAndUpdateUserProfile(userId, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide et met à jour le mot de passe avec toutes les règles métier
|
||||||
|
*/
|
||||||
|
async validateAndUpdatePassword(
|
||||||
|
userId: string,
|
||||||
|
currentPassword: string,
|
||||||
|
newPassword: string,
|
||||||
|
confirmPassword: string
|
||||||
|
): Promise<void> {
|
||||||
|
// Validation des champs requis
|
||||||
|
if (!currentPassword || !newPassword || !confirmPassword) {
|
||||||
|
throw new ValidationError("Tous les champs sont requis");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation du nouveau mot de passe
|
||||||
|
if (newPassword.length < PASSWORD_MIN_LENGTH) {
|
||||||
|
throw new ValidationError(
|
||||||
|
`Le nouveau mot de passe doit contenir au moins ${PASSWORD_MIN_LENGTH} caractères`,
|
||||||
|
"newPassword"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que les mots de passe correspondent
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
throw new ValidationError(
|
||||||
|
"Les mots de passe ne correspondent pas",
|
||||||
|
"confirmPassword"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour le mot de passe (la méthode updateUserPassword gère déjà la vérification de l'ancien mot de passe)
|
||||||
|
await this.updateUserPassword(userId, currentPassword, newPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide et supprime un utilisateur avec vérification des règles métier
|
||||||
|
*/
|
||||||
|
async validateAndDeleteUser(
|
||||||
|
userId: string,
|
||||||
|
currentUserId: string
|
||||||
|
): Promise<void> {
|
||||||
|
// Vérifier que l'utilisateur existe
|
||||||
|
const user = await this.getUserById(userId);
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundError("Utilisateur");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empêcher la suppression de soi-même
|
||||||
|
if (user.id === currentUserId) {
|
||||||
|
throw new ValidationError(
|
||||||
|
"Vous ne pouvez pas supprimer votre propre compte"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprimer l'utilisateur
|
||||||
|
await this.deleteUser(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userService = new UserService();
|
||||||
Reference in New Issue
Block a user