refactor: update authentication flow and cookie management

- Changed COOKIE_NAME from "peakSkills_userId" to "session_token" for better clarity.
- Updated AuthClient to handle login and registration with new data structures.
- Enhanced AuthWrapper to manage user sessions and display appropriate messages.
- Added error handling in LoginForm and RegisterForm for better user feedback.
- Refactored user service methods to streamline user creation and verification processes.
This commit is contained in:
Julien Froidefond
2025-08-25 16:19:31 +02:00
parent caf396d964
commit 5c71ce1a54
14 changed files with 537 additions and 91 deletions

View File

@@ -0,0 +1,62 @@
import { NextRequest, NextResponse } from "next/server";
import { AuthService, UserService } from "@/services";
export async function POST(request: NextRequest) {
try {
const { email, password } = await request.json();
// Validation des données
if (!email || !password) {
return NextResponse.json(
{ error: "Email et mot de passe requis" },
{ status: 400 }
);
}
// Vérifier les identifiants
const userService = new UserService();
const user = await userService.verifyCredentials(email, password);
if (!user) {
return NextResponse.json(
{ error: "Email ou mot de passe incorrect" },
{ status: 401 }
);
}
// Générer un token de session
const sessionToken = await AuthService.createSession(user.uuid_id);
// Créer la réponse avec le cookie de session
const response = NextResponse.json(
{
message: "Connexion réussie",
user: {
id: user.uuid_id,
firstName: user.first_name,
lastName: user.last_name,
email: user.email,
teamId: user.team_id,
},
},
{ status: 200 }
);
// Définir le cookie de session
response.cookies.set("session_token", sessionToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 60 * 60 * 24 * 7, // 7 jours
path: "/",
});
return response;
} catch (error) {
console.error("Login error:", error);
return NextResponse.json(
{ error: "Erreur interne du serveur" },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,29 @@
import { NextResponse } from "next/server";
export async function POST() {
try {
// Créer la réponse
const response = NextResponse.json(
{ message: "Déconnexion réussie" },
{ status: 200 }
);
// Supprimer le cookie de session
response.cookies.set("session_token", "", {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 0, // Expire immédiatement
path: "/",
});
return response;
} catch (error) {
console.error("Logout error:", error);
return NextResponse.json(
{ error: "Erreur interne du serveur" },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,83 @@
import { NextRequest, NextResponse } from "next/server";
import { AuthService, userService } from "@/services";
import bcrypt from "bcryptjs";
export async function POST(request: NextRequest) {
try {
const { firstName, lastName, email, password, teamId } =
await request.json();
// Validation des données
if (!firstName || !lastName || !email || !password || !teamId) {
return NextResponse.json(
{ error: "Tous les champs sont requis" },
{ status: 400 }
);
}
// Vérifier si l'email existe déjà
const existingUser = await userService.getUserByEmail(email);
if (existingUser) {
return NextResponse.json(
{ error: "Un utilisateur avec cet email existe déjà" },
{ status: 409 }
);
}
// Hasher le mot de passe
const saltRounds = 12;
const passwordHash = await bcrypt.hash(password, saltRounds);
// Créer l'utilisateur
const newUser = await userService.createUser({
firstName,
lastName,
email,
passwordHash,
teamId,
});
if (!newUser) {
return NextResponse.json(
{ error: "Erreur lors de la création de l'utilisateur" },
{ status: 500 }
);
}
// Générer un token de session
const sessionToken = await AuthService.createSession(newUser.uuid_id);
// Créer la réponse avec le cookie de session
const response = NextResponse.json(
{
message: "Compte créé avec succès",
user: {
id: newUser.uuid_id,
firstName: newUser.first_name,
lastName: newUser.last_name,
email: newUser.email,
teamId: newUser.team_id,
},
},
{ status: 201 }
);
// Définir le cookie de session
response.cookies.set("session_token", sessionToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 60 * 60 * 24 * 7, // 7 jours
path: "/",
});
return response;
} catch (error) {
console.error("Register error:", error);
return NextResponse.json(
{ error: "Erreur interne du serveur" },
{ status: 500 }
);
}
}

View File

@@ -1,4 +1,3 @@
import { redirect } from "next/navigation";
import { TeamsService, userService } from "@/services"; import { TeamsService, userService } from "@/services";
import { AuthService } from "@/services"; import { AuthService } from "@/services";
import { LoginLayout, AuthWrapper, LoginLoading } from "@/components/login"; import { LoginLayout, AuthWrapper, LoginLoading } from "@/components/login";
@@ -11,20 +10,17 @@ export default async function LoginPage() {
// Vérifier si l'utilisateur est déjà connecté // Vérifier si l'utilisateur est déjà connecté
const userUuid = await AuthService.getUserUuidFromCookie(); const userUuid = await AuthService.getUserUuidFromCookie();
let userProfile = null;
if (userUuid) { if (userUuid) {
// Si l'utilisateur est connecté, récupérer son profil côté serveur // Si l'utilisateur est connecté, récupérer son profil côté serveur
const userProfile = await userService.getUserByUuid(userUuid); userProfile = await userService.getUserByUuid(userUuid);
if (userProfile) {
// Rediriger vers l'accueil si déjà connecté
redirect("/");
}
} }
// Si l'utilisateur n'est pas connecté, afficher le formulaire d'auth // Si l'utilisateur n'est pas connecté, afficher le formulaire d'auth
return ( return (
<LoginLayout> <LoginLayout>
<AuthWrapper teams={teams} /> <AuthWrapper teams={teams} initialUser={userProfile} />
</LoginLayout> </LoginLayout>
); );
} catch (error) { } catch (error) {

View File

@@ -1,14 +1,51 @@
import { BaseHttpClient } from "../base/http-client"; import { BaseHttpClient } from "../base/http-client";
import { UserProfile } from "../../lib/types"; import { UserProfile } from "../../lib/types";
export interface LoginCredentials {
email: string;
password: string;
}
export interface RegisterData {
firstName: string;
lastName: string;
email: string;
password: string;
teamId: string;
}
export interface AuthUser {
id: string;
firstName: string;
lastName: string;
email: string;
teamId: string;
}
export class AuthClient extends BaseHttpClient { export class AuthClient extends BaseHttpClient {
/** /**
* Authentifie un utilisateur et créé le cookie * Connecte un utilisateur avec email/password
*/ */
async login( async login(
profile: UserProfile credentials: LoginCredentials
): Promise<{ user: UserProfile & { uuid: string }; userUuid: string }> { ): Promise<{ user: AuthUser; message: string }> {
return await this.post("/auth", profile); return await this.post("/auth/login", credentials);
}
/**
* Crée un nouveau compte utilisateur
*/
async register(
data: RegisterData
): Promise<{ user: AuthUser; message: string }> {
return await this.post("/auth/register", data);
}
/**
* Déconnecte l'utilisateur
*/
async logout(): Promise<{ message: string }> {
return await this.post("/auth/logout");
} }
/** /**
@@ -23,11 +60,4 @@ export class AuthClient extends BaseHttpClient {
return null; return null;
} }
} }
/**
* Déconnecte l'utilisateur (supprime le cookie)
*/
async logout(): Promise<void> {
await this.delete("/auth");
}
} }

View File

@@ -1,24 +1,56 @@
"use client"; "use client";
import { useState } from "react"; import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { LoginForm, RegisterForm } from "./index"; import { LoginForm, RegisterForm } from "./index";
import { authClient } from "@/clients";
import { useToast } from "@/hooks/use-toast";
import { UserProfile } from "@/lib/types";
interface AuthWrapperProps { interface AuthWrapperProps {
teams: any[]; teams: any[];
initialUser?: UserProfile | null;
} }
export function AuthWrapper({ teams }: AuthWrapperProps) { export function AuthWrapper({ teams, initialUser }: AuthWrapperProps) {
const [isLogin, setIsLogin] = useState(true); const [isLogin, setIsLogin] = useState(true);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const { toast } = useToast();
// Rediriger si l'utilisateur est déjà connecté
useEffect(() => {
// Temporairement désactivé pour éviter la boucle
// if (initialUser) {
// router.push("/");
// }
}, [initialUser, router]);
const handleLogin = async (email: string, password: string) => { const handleLogin = async (email: string, password: string) => {
setLoading(true); setLoading(true);
setError(null);
try { try {
// TODO: Implémenter la logique de login const response = await authClient.login({ email, password });
console.log("Login attempt:", { email, password });
// await authClient.login(email, password); toast({
} catch (error) { title: "Connexion réussie",
description: response.message,
});
// Rediriger vers l'accueil après connexion
router.push("/");
} catch (error: any) {
console.error("Login failed:", error); console.error("Login failed:", error);
const errorMessage = error.response?.data?.error || "Erreur de connexion";
setError(errorMessage);
toast({
title: "Erreur de connexion",
description: errorMessage,
variant: "destructive",
});
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -32,23 +64,88 @@ export function AuthWrapper({ teams }: AuthWrapperProps) {
teamId: string; teamId: string;
}) => { }) => {
setLoading(true); setLoading(true);
setError(null);
try { try {
// TODO: Implémenter la logique de register const response = await authClient.register(data);
console.log("Register attempt:", data);
// await authClient.register(data); toast({
} catch (error) { title: "Compte créé",
description: response.message,
});
// Basculer vers le login après inscription réussie
setIsLogin(true);
} catch (error: any) {
console.error("Register failed:", error); console.error("Register failed:", error);
const errorMessage =
error.response?.data?.error || "Erreur lors de la création du compte";
setError(errorMessage);
toast({
title: "Erreur d'inscription",
description: errorMessage,
variant: "destructive",
});
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const handleLogout = async () => {
try {
await authClient.logout();
// Rediriger vers la page de login après déconnexion
window.location.href = "/login";
} catch (error) {
console.error("Logout failed:", error);
// En cas d'erreur, forcer le rechargement pour nettoyer l'état
window.location.reload();
}
};
// Si l'utilisateur est déjà connecté, afficher un message au lieu de rediriger
if (initialUser) {
return (
<div className="text-center mb-12">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 backdrop-blur-sm mb-6">
<span className="text-sm font-medium text-slate-200">PeakSkills</span>
</div>
<h1 className="text-4xl font-bold mb-4 text-white">
Vous êtes déjà connecté
</h1>
<p className="text-lg text-slate-400 mb-8">
Bonjour {initialUser.firstName} {initialUser.lastName}
</p>
<div className="max-w-2xl mx-auto space-y-6">
<div className="flex gap-4 justify-center">
<button
onClick={() => (window.location.href = "/")}
className="px-6 py-3 bg-blue-500 hover:bg-blue-600 text-white rounded-lg"
>
Aller à l'accueil
</button>
<button
onClick={handleLogout}
className="px-6 py-3 bg-gray-500 hover:bg-gray-600 text-white rounded-lg"
>
Se déconnecter
</button>
</div>
</div>
</div>
);
}
if (isLogin) { if (isLogin) {
return ( return (
<LoginForm <LoginForm
onSubmit={handleLogin} onSubmit={handleLogin}
onSwitchToRegister={() => setIsLogin(false)} onSwitchToRegister={() => setIsLogin(false)}
loading={loading} loading={loading}
error={error}
/> />
); );
} }
@@ -59,6 +156,7 @@ export function AuthWrapper({ teams }: AuthWrapperProps) {
onSubmit={handleRegister} onSubmit={handleRegister}
onSwitchToLogin={() => setIsLogin(true)} onSwitchToLogin={() => setIsLogin(true)}
loading={loading} loading={loading}
error={error}
/> />
); );
} }

View File

@@ -17,12 +17,14 @@ interface LoginFormProps {
onSubmit: (email: string, password: string) => void; onSubmit: (email: string, password: string) => void;
onSwitchToRegister: () => void; onSwitchToRegister: () => void;
loading?: boolean; loading?: boolean;
error?: string | null;
} }
export function LoginForm({ export function LoginForm({
onSubmit, onSubmit,
onSwitchToRegister, onSwitchToRegister,
loading = false, loading = false,
error = null,
}: LoginFormProps) { }: LoginFormProps) {
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
@@ -57,6 +59,12 @@ export function LoginForm({
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{error && (
<div className="mb-4 p-3 bg-red-500/20 border border-red-500/50 rounded-md">
<p className="text-red-400 text-sm">{error}</p>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="email" className="text-slate-300"> <Label htmlFor="email" className="text-slate-300">

View File

@@ -25,6 +25,7 @@ interface RegisterFormProps {
}) => void; }) => void;
onSwitchToLogin: () => void; onSwitchToLogin: () => void;
loading?: boolean; loading?: boolean;
error?: string | null;
} }
export function RegisterForm({ export function RegisterForm({
@@ -32,6 +33,7 @@ export function RegisterForm({
onSubmit, onSubmit,
onSwitchToLogin, onSwitchToLogin,
loading = false, loading = false,
error = null,
}: RegisterFormProps) { }: RegisterFormProps) {
const [firstName, setFirstName] = useState(""); const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState(""); const [lastName, setLastName] = useState("");
@@ -138,6 +140,12 @@ export function RegisterForm({
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{error && (
<div className="mb-4 p-3 bg-red-500/20 border border-red-500/50 rounded-md">
<p className="text-red-400 text-sm">{error}</p>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
const COOKIE_NAME = "peakSkills_userId"; const COOKIE_NAME = "session_token";
export function middleware(request: NextRequest) { export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl; const { pathname } = request.nextUrl;

View File

@@ -45,9 +45,11 @@
"@radix-ui/react-toggle-group": "1.1.1", "@radix-ui/react-toggle-group": "1.1.1",
"@radix-ui/react-tooltip": "1.1.6", "@radix-ui/react-tooltip": "1.1.6",
"@types/bcrypt": "^6.0.0", "@types/bcrypt": "^6.0.0",
"@types/bcryptjs": "^3.0.0",
"@types/pg": "^8.11.10", "@types/pg": "^8.11.10",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"bcryptjs": "^3.0.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "1.0.4", "cmdk": "1.0.4",

20
pnpm-lock.yaml generated
View File

@@ -107,6 +107,9 @@ importers:
'@types/bcrypt': '@types/bcrypt':
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.0 version: 6.0.0
'@types/bcryptjs':
specifier: ^3.0.0
version: 3.0.0
'@types/pg': '@types/pg':
specifier: ^8.11.10 specifier: ^8.11.10
version: 8.15.5 version: 8.15.5
@@ -116,6 +119,9 @@ importers:
bcrypt: bcrypt:
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.0 version: 6.0.0
bcryptjs:
specifier: ^3.0.2
version: 3.0.2
class-variance-authority: class-variance-authority:
specifier: ^0.7.1 specifier: ^0.7.1
version: 0.7.1 version: 0.7.1
@@ -1354,6 +1360,10 @@ packages:
'@types/bcrypt@6.0.0': '@types/bcrypt@6.0.0':
resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==} resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==}
'@types/bcryptjs@3.0.0':
resolution: {integrity: sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==}
deprecated: This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed.
'@types/d3-array@3.2.1': '@types/d3-array@3.2.1':
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
@@ -1410,6 +1420,10 @@ packages:
resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==} resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==}
engines: {node: '>= 18'} engines: {node: '>= 18'}
bcryptjs@3.0.2:
resolution: {integrity: sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==}
hasBin: true
browserslist@4.25.3: browserslist@4.25.3:
resolution: {integrity: sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==} resolution: {integrity: sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
@@ -3067,6 +3081,10 @@ snapshots:
dependencies: dependencies:
'@types/node': 22.17.2 '@types/node': 22.17.2
'@types/bcryptjs@3.0.0':
dependencies:
bcryptjs: 3.0.2
'@types/d3-array@3.2.1': {} '@types/d3-array@3.2.1': {}
'@types/d3-color@3.1.3': {} '@types/d3-color@3.1.3': {}
@@ -3128,6 +3146,8 @@ snapshots:
node-addon-api: 8.5.0 node-addon-api: 8.5.0
node-gyp-build: 4.8.4 node-gyp-build: 4.8.4
bcryptjs@3.0.2: {}
browserslist@4.25.3: browserslist@4.25.3:
dependencies: dependencies:
caniuse-lite: 1.0.30001735 caniuse-lite: 1.0.30001735

View File

@@ -2,8 +2,8 @@ import { cookies } from "next/headers";
import { UserProfile } from "@/lib/types"; import { UserProfile } from "@/lib/types";
import { userService } from "@/services/user-service"; import { userService } from "@/services/user-service";
// Constantes pour les cookies (définies ici car auth-service.ts a été supprimé) // Constantes pour les cookies
export const COOKIE_NAME = "peakSkills_userId"; export const COOKIE_NAME = "session_token";
export const COOKIE_MAX_AGE = 30 * 24 * 60 * 60; // 30 jours export const COOKIE_MAX_AGE = 30 * 24 * 60 * 60; // 30 jours
/** /**
@@ -61,6 +61,8 @@ export class AuthService {
/** /**
* Authentifie un utilisateur et retourne la configuration du cookie * Authentifie un utilisateur et retourne la configuration du cookie
* Note: Cette méthode est maintenant obsolète avec le nouveau système d'auth
* Utilisez login/register à la place
*/ */
static async authenticateUser(profile: UserProfile): Promise<{ static async authenticateUser(profile: UserProfile): Promise<{
userUuid: string; userUuid: string;
@@ -78,24 +80,18 @@ export class AuthService {
}> { }> {
// Vérifier si l'utilisateur existe déjà avec ces informations // Vérifier si l'utilisateur existe déjà avec ces informations
const existingUser = await userService.findUserByProfile(profile); const existingUser = await userService.findUserByProfile(profile);
let userUuid: string;
if (existingUser) { if (!existingUser) {
// Mettre à jour l'utilisateur existant si nécessaire throw new Error(
if (existingUser.teamId !== profile.teamId) { "Utilisateur non trouvé. Veuillez vous connecter avec votre email et mot de passe."
await userService.updateUserByUuid(existingUser.uuid, profile); );
}
userUuid = existingUser.uuid;
} else {
// Créer un nouvel utilisateur
userUuid = await userService.upsertUserUuid(profile);
} }
return { return {
userUuid, userUuid: existingUser.uuid,
cookieConfig: { cookieConfig: {
name: COOKIE_NAME, name: COOKIE_NAME,
value: userUuid, value: existingUser.uuid,
options: { options: {
maxAge: COOKIE_MAX_AGE, maxAge: COOKIE_MAX_AGE,
httpOnly: true, httpOnly: true,
@@ -106,4 +102,25 @@ export class AuthService {
}, },
}; };
} }
/**
* Crée une nouvelle session pour un utilisateur
*/
static async createSession(userUuid: string): Promise<string> {
// Pour l'instant, on utilise l'UUID comme token de session
// Plus tard, on pourra implémenter un système de JWT ou de sessions en base
return userUuid;
}
/**
* Valide un token de session et retourne l'UUID utilisateur
*/
static async validateSession(sessionToken: string): Promise<string | null> {
// Pour l'instant, on considère que le token est valide s'il correspond à un UUID
// Plus tard, on pourra ajouter une validation plus robuste
if (sessionToken && sessionToken.length > 0) {
return sessionToken;
}
return null;
}
} }

View File

@@ -1,5 +1,6 @@
import { getPool } from "./database"; import { getPool } from "./database";
import { userService } from "./user-service"; import { userService } from "./user-service";
import { AuthService } from "./auth-service";
import { import {
UserEvaluation, UserEvaluation,
UserProfile, UserProfile,
@@ -121,7 +122,7 @@ export class EvaluationService {
await client.query("BEGIN"); await client.query("BEGIN");
// 1. Upsert user avec UUID // 1. Upsert user avec UUID
const userUuid = await userService.upsertUserUuid(evaluation.profile); const userUuid = await AuthService.getUserUuidFromCookie();
// 2. Upsert user_evaluation avec user_uuid // 2. Upsert user_evaluation avec user_uuid
const userEvalQuery = ` const userEvalQuery = `
@@ -413,7 +414,15 @@ export class EvaluationService {
try { try {
await client.query("BEGIN"); await client.query("BEGIN");
const userUuid = await userService.upsertUserUuid(profile); // Trouver l'utilisateur existant au lieu d'en créer un nouveau
const existingUser = await userService.findUserByProfile(profile);
if (!existingUser) {
throw new Error(
"Utilisateur non trouvé. Veuillez vous connecter avec votre email et mot de passe."
);
}
const userUuid = existingUser.uuid;
// Upsert user_evaluation avec user_uuid // Upsert user_evaluation avec user_uuid
const userEvalResult = await client.query( const userEvalResult = await client.query(
@@ -609,7 +618,7 @@ export class EvaluationService {
try { try {
await client.query("BEGIN"); await client.query("BEGIN");
const userUuid = await userService.upsertUserUuid(profile); const userUuid = await AuthService.getUserUuidFromCookie();
// Supprimer directement la skill evaluation // Supprimer directement la skill evaluation
const deleteQuery = ` const deleteQuery = `

View File

@@ -4,51 +4,9 @@ import { UserProfile } from "../lib/types";
export class UserService { export class UserService {
/** /**
* Crée ou met à jour un utilisateur et retourne son UUID * Crée ou met à jour un utilisateur et retourne son UUID
* Note: Cette méthode est pour la compatibilité avec l'ancien système
* Les nouveaux utilisateurs doivent utiliser createUser avec email/password
*/ */
async upsertUserUuid(profile: UserProfile): Promise<string> {
const pool = getPool();
const client = await pool.connect();
try {
// Créer un nouvel utilisateur avec UUID auto-généré
const insertQuery = `
INSERT INTO users (first_name, last_name, team_id, uuid_id)
VALUES ($1, $2, $3, uuid_generate_v4())
RETURNING uuid_id
`;
const result = await client.query(insertQuery, [
profile.firstName,
profile.lastName,
profile.teamId,
]);
return result.rows[0].uuid_id;
} catch (error: any) {
// Si erreur de contrainte unique, l'utilisateur existe déjà
if (
error.code === "23505" &&
error.constraint === "users_first_name_last_name_team_id_key"
) {
// Récupérer l'utilisateur existant
const existingUserQuery = `
SELECT uuid_id FROM users
WHERE first_name = $1 AND last_name = $2 AND team_id = $3
`;
const existingUser = await client.query(existingUserQuery, [
profile.firstName,
profile.lastName,
profile.teamId,
]);
return existingUser.rows[0].uuid_id;
}
throw error;
} finally {
client.release();
}
}
/** /**
* Met à jour un utilisateur existant par son UUID * Met à jour un utilisateur existant par son UUID
@@ -314,6 +272,132 @@ export class UserService {
throw new Error("Failed to fetch users"); throw new Error("Failed to fetch users");
} }
} }
/**
* Récupère un utilisateur par son email
*/
async getUserByEmail(email: string): Promise<{
uuid_id: string;
first_name: string;
last_name: string;
email: string;
team_id: string;
} | null> {
const pool = getPool();
const client = await pool.connect();
try {
const query = `
SELECT uuid_id, first_name, last_name, email, team_id
FROM users
WHERE email = $1
`;
const result = await client.query(query, [email]);
if (result.rows.length === 0) {
return null;
}
return result.rows[0];
} finally {
client.release();
}
}
/**
* Crée un nouvel utilisateur avec email et mot de passe hashé
*/
async createUser(data: {
firstName: string;
lastName: string;
email: string;
passwordHash: string;
teamId: string;
}): Promise<{
uuid_id: string;
first_name: string;
last_name: string;
email: string;
team_id: string;
} | null> {
const pool = getPool();
const client = await pool.connect();
try {
const query = `
INSERT INTO users (first_name, last_name, email, password_hash, team_id, uuid_id)
VALUES ($1, $2, $3, $4, $5, uuid_generate_v4())
RETURNING uuid_id, first_name, last_name, email, team_id
`;
const result = await client.query(query, [
data.firstName,
data.lastName,
data.email,
data.passwordHash,
data.teamId,
]);
if (result.rows.length === 0) {
return null;
}
return result.rows[0];
} finally {
client.release();
}
}
/**
* Vérifie les identifiants de connexion
*/
async verifyCredentials(
email: string,
password: string
): Promise<{
uuid_id: string;
first_name: string;
last_name: string;
email: string;
team_id: string;
} | null> {
const pool = getPool();
const client = await pool.connect();
try {
const query = `
SELECT uuid_id, first_name, last_name, email, team_id, password_hash
FROM users
WHERE email = $1
`;
const result = await client.query(query, [email]);
if (result.rows.length === 0) {
return null;
}
const user = result.rows[0];
// Vérifier le mot de passe
const bcrypt = require("bcryptjs");
const isValidPassword = await bcrypt.compare(
password,
user.password_hash
);
if (!isValidPassword) {
return null;
}
// Retourner l'utilisateur sans le hash du mot de passe
const { password_hash, ...userWithoutPassword } = user;
return userWithoutPassword;
} finally {
client.release();
}
}
} }
// Instance singleton // Instance singleton