From 5c71ce1a542caf9af6a274ff49f0871c2bfb8cb7 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Mon, 25 Aug 2025 16:19:31 +0200 Subject: [PATCH] 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. --- app/api/auth/login/route.ts | 62 +++++++++++ app/api/auth/logout/route.ts | 29 +++++ app/api/auth/register/route.ts | 83 ++++++++++++++ app/login/page.tsx | 12 +- clients/domains/auth-client.ts | 52 +++++++-- components/login/auth-wrapper.tsx | 118 ++++++++++++++++++-- components/login/login-form.tsx | 8 ++ components/login/register-form.tsx | 8 ++ middleware.ts | 2 +- package.json | 2 + pnpm-lock.yaml | 20 ++++ services/auth-service.ts | 45 +++++--- services/evaluation-service.ts | 15 ++- services/user-service.ts | 172 +++++++++++++++++++++-------- 14 files changed, 537 insertions(+), 91 deletions(-) create mode 100644 app/api/auth/login/route.ts create mode 100644 app/api/auth/logout/route.ts create mode 100644 app/api/auth/register/route.ts diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts new file mode 100644 index 0000000..a69d4f6 --- /dev/null +++ b/app/api/auth/login/route.ts @@ -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 } + ); + } +} diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts new file mode 100644 index 0000000..0330a98 --- /dev/null +++ b/app/api/auth/logout/route.ts @@ -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 } + ); + } +} + diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts new file mode 100644 index 0000000..782efe3 --- /dev/null +++ b/app/api/auth/register/route.ts @@ -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 } + ); + } +} + diff --git a/app/login/page.tsx b/app/login/page.tsx index 2bdce7a..435697c 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,4 +1,3 @@ -import { redirect } from "next/navigation"; import { TeamsService, userService } from "@/services"; import { AuthService } from "@/services"; 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é const userUuid = await AuthService.getUserUuidFromCookie(); + let userProfile = null; + if (userUuid) { // Si l'utilisateur est connecté, récupérer son profil côté serveur - const userProfile = await userService.getUserByUuid(userUuid); - - if (userProfile) { - // Rediriger vers l'accueil si déjà connecté - redirect("/"); - } + userProfile = await userService.getUserByUuid(userUuid); } // Si l'utilisateur n'est pas connecté, afficher le formulaire d'auth return ( - + ); } catch (error) { diff --git a/clients/domains/auth-client.ts b/clients/domains/auth-client.ts index cdaaace..a4fea7b 100644 --- a/clients/domains/auth-client.ts +++ b/clients/domains/auth-client.ts @@ -1,14 +1,51 @@ import { BaseHttpClient } from "../base/http-client"; 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 { /** - * Authentifie un utilisateur et créé le cookie + * Connecte un utilisateur avec email/password */ async login( - profile: UserProfile - ): Promise<{ user: UserProfile & { uuid: string }; userUuid: string }> { - return await this.post("/auth", profile); + credentials: LoginCredentials + ): Promise<{ user: AuthUser; message: string }> { + 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; } } - - /** - * Déconnecte l'utilisateur (supprime le cookie) - */ - async logout(): Promise { - await this.delete("/auth"); - } } diff --git a/components/login/auth-wrapper.tsx b/components/login/auth-wrapper.tsx index ac56897..33de413 100644 --- a/components/login/auth-wrapper.tsx +++ b/components/login/auth-wrapper.tsx @@ -1,24 +1,56 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; import { LoginForm, RegisterForm } from "./index"; +import { authClient } from "@/clients"; +import { useToast } from "@/hooks/use-toast"; +import { UserProfile } from "@/lib/types"; interface AuthWrapperProps { teams: any[]; + initialUser?: UserProfile | null; } -export function AuthWrapper({ teams }: AuthWrapperProps) { +export function AuthWrapper({ teams, initialUser }: AuthWrapperProps) { const [isLogin, setIsLogin] = useState(true); const [loading, setLoading] = useState(false); + const [error, setError] = useState(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) => { setLoading(true); + setError(null); try { - // TODO: Implémenter la logique de login - console.log("Login attempt:", { email, password }); - // await authClient.login(email, password); - } catch (error) { + const response = await authClient.login({ email, password }); + + toast({ + title: "Connexion réussie", + description: response.message, + }); + + // Rediriger vers l'accueil après connexion + router.push("/"); + } catch (error: any) { 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 { setLoading(false); } @@ -32,23 +64,88 @@ export function AuthWrapper({ teams }: AuthWrapperProps) { teamId: string; }) => { setLoading(true); + setError(null); try { - // TODO: Implémenter la logique de register - console.log("Register attempt:", data); - // await authClient.register(data); - } catch (error) { + const response = await authClient.register(data); + + toast({ + 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); + + 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 { 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 ( +
+
+ PeakSkills +
+ +

+ Vous êtes déjà connecté +

+

+ Bonjour {initialUser.firstName} {initialUser.lastName} +

+ +
+
+ + +
+
+
+ ); + } + if (isLogin) { return ( setIsLogin(false)} loading={loading} + error={error} /> ); } @@ -59,6 +156,7 @@ export function AuthWrapper({ teams }: AuthWrapperProps) { onSubmit={handleRegister} onSwitchToLogin={() => setIsLogin(true)} loading={loading} + error={error} /> ); } diff --git a/components/login/login-form.tsx b/components/login/login-form.tsx index c29e397..c0b36d9 100644 --- a/components/login/login-form.tsx +++ b/components/login/login-form.tsx @@ -17,12 +17,14 @@ interface LoginFormProps { onSubmit: (email: string, password: string) => void; onSwitchToRegister: () => void; loading?: boolean; + error?: string | null; } export function LoginForm({ onSubmit, onSwitchToRegister, loading = false, + error = null, }: LoginFormProps) { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); @@ -57,6 +59,12 @@ export function LoginForm({ + {error && ( +
+

{error}

+
+ )} +