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.
This commit is contained in:
Julien Froidefond
2025-08-21 11:55:50 +02:00
parent 5cb2bad992
commit 45fb1148ae
6 changed files with 270 additions and 32 deletions

98
app/api/auth/route.ts Normal file
View File

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

View File

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

View File

@@ -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);
@@ -136,7 +134,10 @@ export function useEvaluation() {
// 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 {

71
lib/auth-utils.ts Normal file
View File

@@ -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<UserProfile | null> {
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<void> {
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

View File

@@ -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<UserEvaluation | null> {
try {
const params = new URLSearchParams({
firstName: profile.firstName,
lastName: profile.lastName,
teamId: profile.teamId,
});
let url = `${this.baseUrl}/api/evaluations`;
const response = await fetch(`${this.baseUrl}/api/evaluations?${params}`);
// 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(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) {

View File

@@ -66,6 +66,37 @@ export class EvaluationService {
}
}
/**
* Récupère un utilisateur par son ID
*/
async getUserById(userId: number): Promise<UserProfile | null> {
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
*/