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:
62
app/api/auth/login/route.ts
Normal file
62
app/api/auth/login/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
app/api/auth/logout/route.ts
Normal file
29
app/api/auth/logout/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
83
app/api/auth/register/route.ts
Normal file
83
app/api/auth/register/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
20
pnpm-lock.yaml
generated
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = `
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user