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:
98
app/api/auth/route.ts
Normal file
98
app/api/auth/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
import { evaluationService } from "@/services/evaluation-service";
|
import { evaluationService } from "@/services/evaluation-service";
|
||||||
import { UserEvaluation, UserProfile } from "@/lib/types";
|
import { UserEvaluation, UserProfile } from "@/lib/types";
|
||||||
|
import { COOKIE_NAME } from "@/lib/auth-utils";
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const userId = cookieStore.get(COOKIE_NAME)?.value;
|
||||||
|
const userIdNum = userId ? parseInt(userId) : null;
|
||||||
|
|
||||||
|
// Support pour l'ancien mode avec paramètres (pour la compatibilité)
|
||||||
|
if (!userIdNum) {
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const firstName = searchParams.get("firstName");
|
const firstName = searchParams.get("firstName");
|
||||||
const lastName = searchParams.get("lastName");
|
const lastName = searchParams.get("lastName");
|
||||||
@@ -11,14 +19,26 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
if (!firstName || !lastName || !teamId) {
|
if (!firstName || !lastName || !teamId) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "firstName, lastName et teamId sont requis" },
|
{ error: "Utilisateur non authentifié" },
|
||||||
{ status: 400 }
|
{ status: 401 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const profile: UserProfile = { firstName, lastName, teamId };
|
const profile: UserProfile = { firstName, lastName, teamId };
|
||||||
const evaluation = await evaluationService.loadUserEvaluation(profile);
|
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: "Utilisateur non trouvé" },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const evaluation = await evaluationService.loadUserEvaluation(userProfile);
|
||||||
return NextResponse.json({ evaluation });
|
return NextResponse.json({ evaluation });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors du chargement de l'évaluation:", error);
|
console.error("Erreur lors du chargement de l'évaluation:", error);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from "@/lib/evaluation-utils";
|
} from "@/lib/evaluation-utils";
|
||||||
import { apiClient } from "@/services/api-client";
|
import { apiClient } from "@/services/api-client";
|
||||||
import { loadSkillCategories, loadTeams } from "@/lib/data-loader";
|
import { loadSkillCategories, loadTeams } from "@/lib/data-loader";
|
||||||
|
import { AuthService } from "@/lib/auth-utils";
|
||||||
|
|
||||||
// Fonction pour migrer une évaluation existante avec de nouvelles catégories
|
// Fonction pour migrer une évaluation existante avec de nouvelles catégories
|
||||||
function migrateEvaluation(
|
function migrateEvaluation(
|
||||||
@@ -71,11 +72,10 @@ export function useEvaluation() {
|
|||||||
setSkillCategories(categories);
|
setSkillCategories(categories);
|
||||||
setTeams(teamsData);
|
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 {
|
try {
|
||||||
const savedProfile = localStorage.getItem("peakSkills_userProfile");
|
const profile = await AuthService.getCurrentUser();
|
||||||
if (savedProfile) {
|
if (profile) {
|
||||||
const profile: UserProfile = JSON.parse(savedProfile);
|
|
||||||
const saved = await loadUserEvaluation(profile);
|
const saved = await loadUserEvaluation(profile);
|
||||||
if (saved) {
|
if (saved) {
|
||||||
// Migrate evaluation to include new categories if needed
|
// Migrate evaluation to include new categories if needed
|
||||||
@@ -88,8 +88,6 @@ export function useEvaluation() {
|
|||||||
}
|
}
|
||||||
} catch (profileError) {
|
} catch (profileError) {
|
||||||
console.error("Failed to load user profile:", profileError);
|
console.error("Failed to load user profile:", profileError);
|
||||||
// Clear invalid profile data
|
|
||||||
localStorage.removeItem("peakSkills_userProfile");
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to initialize data:", 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
|
// Si on a une évaluation en cours, la migrer avec les nouvelles catégories
|
||||||
if (userEvaluation) {
|
if (userEvaluation) {
|
||||||
const migratedEvaluation = migrateEvaluation(userEvaluation, categories);
|
const migratedEvaluation = migrateEvaluation(
|
||||||
|
userEvaluation,
|
||||||
|
categories
|
||||||
|
);
|
||||||
if (migratedEvaluation !== userEvaluation) {
|
if (migratedEvaluation !== userEvaluation) {
|
||||||
setUserEvaluation(migratedEvaluation);
|
setUserEvaluation(migratedEvaluation);
|
||||||
await saveUserEvaluation(migratedEvaluation);
|
await saveUserEvaluation(migratedEvaluation);
|
||||||
@@ -156,8 +157,8 @@ export function useEvaluation() {
|
|||||||
lastUpdated: new Date().toISOString(),
|
lastUpdated: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save profile to localStorage for auto-login
|
// Authenticate user and create cookie
|
||||||
localStorage.setItem("peakSkills_userProfile", JSON.stringify(profile));
|
await AuthService.login(profile);
|
||||||
|
|
||||||
setUserEvaluation(newEvaluation);
|
setUserEvaluation(newEvaluation);
|
||||||
await saveUserEvaluation(newEvaluation);
|
await saveUserEvaluation(newEvaluation);
|
||||||
@@ -371,16 +372,21 @@ export function useEvaluation() {
|
|||||||
lastUpdated: new Date().toISOString(),
|
lastUpdated: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save profile to localStorage for auto-login
|
// Authenticate user and create cookie
|
||||||
localStorage.setItem("peakSkills_userProfile", JSON.stringify(profile));
|
await AuthService.login(profile);
|
||||||
|
|
||||||
setUserEvaluation(newEvaluation);
|
setUserEvaluation(newEvaluation);
|
||||||
await saveUserEvaluation(newEvaluation);
|
await saveUserEvaluation(newEvaluation);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearUserProfile = () => {
|
const clearUserProfile = async () => {
|
||||||
localStorage.removeItem("peakSkills_userProfile");
|
try {
|
||||||
|
await AuthService.logout();
|
||||||
setUserEvaluation(null);
|
setUserEvaluation(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to logout:", error);
|
||||||
|
setUserEvaluation(null);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
71
lib/auth-utils.ts
Normal file
71
lib/auth-utils.ts
Normal 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
|
||||||
@@ -16,18 +16,28 @@ export class ApiClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Charge une évaluation utilisateur depuis l'API
|
* 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(
|
async loadUserEvaluation(
|
||||||
profile: UserProfile
|
profile?: UserProfile
|
||||||
): Promise<UserEvaluation | null> {
|
): Promise<UserEvaluation | null> {
|
||||||
try {
|
try {
|
||||||
|
let url = `${this.baseUrl}/api/evaluations`;
|
||||||
|
|
||||||
|
// Mode compatibilité avec profile en paramètres
|
||||||
|
if (profile) {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
firstName: profile.firstName,
|
firstName: profile.firstName,
|
||||||
lastName: profile.lastName,
|
lastName: profile.lastName,
|
||||||
teamId: profile.teamId,
|
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) {
|
if (!response.ok) {
|
||||||
throw new Error("Erreur lors du chargement de l'évaluation");
|
throw new Error("Erreur lors du chargement de l'évaluation");
|
||||||
@@ -52,6 +62,7 @@ export class ApiClient {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ evaluation }),
|
body: JSON.stringify({ evaluation }),
|
||||||
|
credentials: "same-origin",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -165,6 +176,7 @@ export class ApiClient {
|
|||||||
skillId,
|
skillId,
|
||||||
...options,
|
...options,
|
||||||
}),
|
}),
|
||||||
|
credentials: "same-origin",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@@ -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
|
* Sauvegarde une évaluation utilisateur complète
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user