From 45fb1148ae0a6ac7d6c7c03ff86ab52fd9ccd901 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Thu, 21 Aug 2025 11:55:50 +0200 Subject: [PATCH] feat: enhance evaluation loading with cookie authentication - Updated the GET method in the evaluations route to support user authentication via cookies, improving security and user experience. - Added compatibility for legacy parameter-based authentication to ensure backward compatibility. - Refactored the useEvaluation hook to load user profiles from cookies instead of localStorage, streamlining the authentication process. - Introduced a new method in EvaluationService to retrieve user profiles by ID, enhancing data retrieval efficiency. - Updated ApiClient to handle cookie-based requests for loading evaluations, ensuring proper session management. --- app/api/auth/route.ts | 98 ++++++++++++++++++++++++++++++++++ app/api/evaluations/route.ts | 40 ++++++++++---- hooks/use-evaluation.ts | 36 +++++++------ lib/auth-utils.ts | 71 ++++++++++++++++++++++++ services/api-client.ts | 26 ++++++--- services/evaluation-service.ts | 31 +++++++++++ 6 files changed, 270 insertions(+), 32 deletions(-) create mode 100644 app/api/auth/route.ts create mode 100644 lib/auth-utils.ts diff --git a/app/api/auth/route.ts b/app/api/auth/route.ts new file mode 100644 index 0000000..179c393 --- /dev/null +++ b/app/api/auth/route.ts @@ -0,0 +1,98 @@ +import { NextRequest, NextResponse } from "next/server"; +import { cookies } from "next/headers"; +import { EvaluationService } from "@/services/evaluation-service"; +import { UserProfile } from "@/lib/types"; + +const COOKIE_NAME = "peakSkills_userId"; +const COOKIE_MAX_AGE = 30 * 24 * 60 * 60; // 30 jours + +/** + * GET /api/auth - Récupère l'utilisateur actuel depuis le cookie + */ +export async function GET() { + try { + const cookieStore = await cookies(); + const userId = cookieStore.get(COOKIE_NAME)?.value; + + if (!userId) { + return NextResponse.json({ user: null }, { status: 200 }); + } + + const evaluationService = new EvaluationService(); + const userProfile = await evaluationService.getUserById(parseInt(userId)); + + if (!userProfile) { + // Cookie invalide, le supprimer + const response = NextResponse.json({ user: null }, { status: 200 }); + response.cookies.set(COOKIE_NAME, "", { maxAge: 0 }); + return response; + } + + return NextResponse.json({ user: userProfile }, { status: 200 }); + } catch (error) { + console.error("Error getting current user:", error); + return NextResponse.json( + { error: "Failed to get current user" }, + { status: 500 } + ); + } +} + +/** + * POST /api/auth - Authentifie un utilisateur et créé/met à jour le cookie + */ +export async function POST(request: NextRequest) { + try { + const profile: UserProfile = await request.json(); + + if (!profile.firstName || !profile.lastName || !profile.teamId) { + return NextResponse.json( + { error: "Missing required fields" }, + { status: 400 } + ); + } + + const evaluationService = new EvaluationService(); + const userId = await evaluationService.upsertUser(profile); + + // Créer la réponse avec le cookie + const response = NextResponse.json({ + user: { ...profile, id: userId }, + userId + }, { status: 200 }); + + // Définir le cookie avec l'ID utilisateur + response.cookies.set(COOKIE_NAME, userId.toString(), { + maxAge: COOKIE_MAX_AGE, + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + path: "/", + }); + + return response; + } catch (error) { + console.error("Error authenticating user:", error); + return NextResponse.json( + { error: "Failed to authenticate user" }, + { status: 500 } + ); + } +} + +/** + * DELETE /api/auth - Déconnecte l'utilisateur (supprime le cookie) + */ +export async function DELETE() { + try { + const response = NextResponse.json({ success: true }, { status: 200 }); + response.cookies.set(COOKIE_NAME, "", { maxAge: 0 }); + return response; + } catch (error) { + console.error("Error logging out user:", error); + return NextResponse.json( + { error: "Failed to logout" }, + { status: 500 } + ); + } +} diff --git a/app/api/evaluations/route.ts b/app/api/evaluations/route.ts index cf9e8dc..d047d20 100644 --- a/app/api/evaluations/route.ts +++ b/app/api/evaluations/route.ts @@ -1,24 +1,44 @@ import { NextRequest, NextResponse } from "next/server"; +import { cookies } from "next/headers"; import { evaluationService } from "@/services/evaluation-service"; import { UserEvaluation, UserProfile } from "@/lib/types"; +import { COOKIE_NAME } from "@/lib/auth-utils"; export async function GET(request: NextRequest) { try { - const { searchParams } = new URL(request.url); - const firstName = searchParams.get("firstName"); - const lastName = searchParams.get("lastName"); - const teamId = searchParams.get("teamId"); + const cookieStore = await cookies(); + const userId = cookieStore.get(COOKIE_NAME)?.value; + const userIdNum = userId ? parseInt(userId) : null; - if (!firstName || !lastName || !teamId) { + // Support pour l'ancien mode avec paramètres (pour la compatibilité) + if (!userIdNum) { + const { searchParams } = new URL(request.url); + const firstName = searchParams.get("firstName"); + const lastName = searchParams.get("lastName"); + const teamId = searchParams.get("teamId"); + + if (!firstName || !lastName || !teamId) { + return NextResponse.json( + { error: "Utilisateur non authentifié" }, + { status: 401 } + ); + } + + const profile: UserProfile = { firstName, lastName, teamId }; + const evaluation = await evaluationService.loadUserEvaluation(profile); + return NextResponse.json({ evaluation }); + } + + // Mode authentifié par cookie + const userProfile = await evaluationService.getUserById(userIdNum); + if (!userProfile) { return NextResponse.json( - { error: "firstName, lastName et teamId sont requis" }, - { status: 400 } + { error: "Utilisateur non trouvé" }, + { status: 404 } ); } - const profile: UserProfile = { firstName, lastName, teamId }; - const evaluation = await evaluationService.loadUserEvaluation(profile); - + const evaluation = await evaluationService.loadUserEvaluation(userProfile); return NextResponse.json({ evaluation }); } catch (error) { console.error("Erreur lors du chargement de l'évaluation:", error); diff --git a/hooks/use-evaluation.ts b/hooks/use-evaluation.ts index 07e3d3c..4226831 100644 --- a/hooks/use-evaluation.ts +++ b/hooks/use-evaluation.ts @@ -16,6 +16,7 @@ import { } from "@/lib/evaluation-utils"; import { apiClient } from "@/services/api-client"; import { loadSkillCategories, loadTeams } from "@/lib/data-loader"; +import { AuthService } from "@/lib/auth-utils"; // Fonction pour migrer une évaluation existante avec de nouvelles catégories function migrateEvaluation( @@ -71,11 +72,10 @@ export function useEvaluation() { setSkillCategories(categories); setTeams(teamsData); - // Try to load user profile from localStorage and then load evaluation from API + // Try to load user profile from cookie and then load evaluation from API try { - const savedProfile = localStorage.getItem("peakSkills_userProfile"); - if (savedProfile) { - const profile: UserProfile = JSON.parse(savedProfile); + const profile = await AuthService.getCurrentUser(); + if (profile) { const saved = await loadUserEvaluation(profile); if (saved) { // Migrate evaluation to include new categories if needed @@ -88,8 +88,6 @@ export function useEvaluation() { } } catch (profileError) { console.error("Failed to load user profile:", profileError); - // Clear invalid profile data - localStorage.removeItem("peakSkills_userProfile"); } } catch (error) { console.error("Failed to initialize data:", error); @@ -133,10 +131,13 @@ export function useEvaluation() { try { const categories = await loadSkillCategories(); setSkillCategories(categories); - + // Si on a une évaluation en cours, la migrer avec les nouvelles catégories if (userEvaluation) { - const migratedEvaluation = migrateEvaluation(userEvaluation, categories); + const migratedEvaluation = migrateEvaluation( + userEvaluation, + categories + ); if (migratedEvaluation !== userEvaluation) { setUserEvaluation(migratedEvaluation); await saveUserEvaluation(migratedEvaluation); @@ -156,8 +157,8 @@ export function useEvaluation() { lastUpdated: new Date().toISOString(), }; - // Save profile to localStorage for auto-login - localStorage.setItem("peakSkills_userProfile", JSON.stringify(profile)); + // Authenticate user and create cookie + await AuthService.login(profile); setUserEvaluation(newEvaluation); await saveUserEvaluation(newEvaluation); @@ -371,16 +372,21 @@ export function useEvaluation() { lastUpdated: new Date().toISOString(), }; - // Save profile to localStorage for auto-login - localStorage.setItem("peakSkills_userProfile", JSON.stringify(profile)); + // Authenticate user and create cookie + await AuthService.login(profile); setUserEvaluation(newEvaluation); await saveUserEvaluation(newEvaluation); }; - const clearUserProfile = () => { - localStorage.removeItem("peakSkills_userProfile"); - setUserEvaluation(null); + const clearUserProfile = async () => { + try { + await AuthService.logout(); + setUserEvaluation(null); + } catch (error) { + console.error("Failed to logout:", error); + setUserEvaluation(null); + } }; return { diff --git a/lib/auth-utils.ts b/lib/auth-utils.ts new file mode 100644 index 0000000..55ddf06 --- /dev/null +++ b/lib/auth-utils.ts @@ -0,0 +1,71 @@ +"use client"; + +import { UserProfile } from "./types"; + +/** + * Service d'authentification côté client + */ +export class AuthService { + /** + * Authentifie un utilisateur et créé le cookie + */ + static async login( + profile: UserProfile + ): Promise<{ user: UserProfile & { id: number }; userId: number }> { + const response = await fetch("/api/auth", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(profile), + }); + + if (!response.ok) { + throw new Error("Failed to authenticate user"); + } + + return response.json(); + } + + /** + * Récupère l'utilisateur actuel depuis le cookie + */ + static async getCurrentUser(): Promise { + try { + const response = await fetch("/api/auth", { + method: "GET", + credentials: "same-origin", + }); + + if (!response.ok) { + return null; + } + + const data = await response.json(); + return data.user; + } catch (error) { + console.error("Failed to get current user:", error); + return null; + } + } + + /** + * Déconnecte l'utilisateur (supprime le cookie) + */ + static async logout(): Promise { + const response = await fetch("/api/auth", { + method: "DELETE", + credentials: "same-origin", + }); + + if (!response.ok) { + throw new Error("Failed to logout"); + } + } +} + +/** + * Constantes pour les cookies + */ +export const COOKIE_NAME = "peakSkills_userId"; +export const COOKIE_MAX_AGE = 30 * 24 * 60 * 60; // 30 jours diff --git a/services/api-client.ts b/services/api-client.ts index a599607..2e2ea06 100644 --- a/services/api-client.ts +++ b/services/api-client.ts @@ -16,18 +16,28 @@ export class ApiClient { /** * Charge une évaluation utilisateur depuis l'API + * Si profile est fourni, utilise les paramètres (mode compatibilité) + * Sinon, utilise l'authentification par cookie */ async loadUserEvaluation( - profile: UserProfile + profile?: UserProfile ): Promise { try { - const params = new URLSearchParams({ - firstName: profile.firstName, - lastName: profile.lastName, - teamId: profile.teamId, - }); + let url = `${this.baseUrl}/api/evaluations`; + + // Mode compatibilité avec profile en paramètres + if (profile) { + const params = new URLSearchParams({ + firstName: profile.firstName, + lastName: profile.lastName, + teamId: profile.teamId, + }); + url += `?${params}`; + } - const response = await fetch(`${this.baseUrl}/api/evaluations?${params}`); + const response = await fetch(url, { + credentials: "same-origin", // Pour inclure les cookies + }); if (!response.ok) { throw new Error("Erreur lors du chargement de l'évaluation"); @@ -52,6 +62,7 @@ export class ApiClient { "Content-Type": "application/json", }, body: JSON.stringify({ evaluation }), + credentials: "same-origin", }); if (!response.ok) { @@ -165,6 +176,7 @@ export class ApiClient { skillId, ...options, }), + credentials: "same-origin", }); if (!response.ok) { diff --git a/services/evaluation-service.ts b/services/evaluation-service.ts index bda260c..4e4a480 100644 --- a/services/evaluation-service.ts +++ b/services/evaluation-service.ts @@ -66,6 +66,37 @@ export class EvaluationService { } } + /** + * Récupère un utilisateur par son ID + */ + async getUserById(userId: number): Promise { + const pool = getPool(); + const client = await pool.connect(); + + try { + const query = ` + SELECT u.first_name, u.last_name, u.team_id + FROM users u + WHERE u.id = $1 + `; + + const result = await client.query(query, [userId]); + + if (result.rows.length === 0) { + return null; + } + + const user = result.rows[0]; + return { + firstName: user.first_name, + lastName: user.last_name, + teamId: user.team_id, + }; + } finally { + client.release(); + } + } + /** * Sauvegarde une évaluation utilisateur complète */