diff --git a/NEXTAUTH_MIGRATION.md b/NEXTAUTH_MIGRATION.md new file mode 100644 index 0000000..2268ef9 --- /dev/null +++ b/NEXTAUTH_MIGRATION.md @@ -0,0 +1,174 @@ +# Migration vers NextAuth v5 - Résumé + +## Changements effectués + +### 1. Installation +- ✅ Installé `next-auth@5.0.0-beta.29` + +### 2. Configuration NextAuth +- ✅ Créé `auth.ts` à la racine avec: + - Credentials provider utilisant `userService.verifyCredentials()` + - Stratégie JWT + - Callbacks pour enrichir la session avec `userId`, `teamId`, `firstName`, `lastName` + - Pages custom (`/login`) + - Middleware callback `authorized` pour protéger les routes + +- ✅ Créé `app/api/auth/[...nextauth]/route.ts` pour les handlers NextAuth + +- ✅ Créé `types/next-auth.d.ts` pour étendre les types Session et User + +### 3. Middleware +- ✅ Remplacé le middleware custom par `export { auth as middleware } from "@/auth"` +- ✅ Les redirections sont gérées dans le callback `authorized` + +### 4. Routes API +- ✅ Supprimé `app/api/auth/login/route.ts` (géré par NextAuth) +- ✅ Supprimé `app/api/auth/logout/route.ts` (géré par NextAuth) +- ✅ Supprimé `app/api/auth/profile/route.ts` (utiliser `useSession()` côté client) +- ✅ Adapté `app/api/auth/register/route.ts` pour retourner les infos sans créer de session +- ✅ Mis à jour toutes les routes protégées pour utiliser `auth()` au lieu de `AuthService.requireAuthenticatedUser()` + +### 5. Services +- ✅ Supprimé `services/auth-service.ts` (remplacé par NextAuth) +- ✅ Mis à jour `services/evaluation-service.ts` pour ne plus utiliser `AuthService` +- ✅ Mis à jour `services/index.ts` pour retirer l'export d'AuthService + +### 6. Clients +- ✅ Mis à jour `clients/domains/auth-client.ts` pour utiliser: + - `signIn("credentials")` pour le login + - `signOut()` pour le logout + - Auto-login après register avec `signIn()` + +### 7. Frontend +- ✅ Créé `components/auth/session-provider.tsx` +- ✅ Wrapped l'application avec `` dans `app/layout.tsx` +- ✅ Mis à jour tous les composants pour utiliser la session NextAuth +- ✅ Mis à jour toutes les pages serveur pour utiliser `auth()` au lieu de `AuthService` + +## Configuration requise + +### Variables d'environnement +Créer un fichier `.env.local` avec: + +```bash +# Générer un secret avec: openssl rand -base64 32 +AUTH_SECRET=your-generated-secret-here + +# Pour la production +# AUTH_URL=https://your-domain.com +``` + +### Pour générer AUTH_SECRET +```bash +openssl rand -base64 32 +``` + +## Utilisation + +### Côté serveur (Server Components, API Routes) +```typescript +import { auth } from "@/auth"; + +export default async function Page() { + const session = await auth(); + + if (!session?.user) { + redirect("/login"); + } + + const userId = session.user.id; + const teamId = session.user.teamId; + const firstName = session.user.firstName; + // ... +} +``` + +### Côté client (Client Components) +```typescript +"use client"; +import { useSession } from "next-auth/react"; +import { signIn, signOut } from "next-auth/react"; + +export function MyComponent() { + const { data: session, status } = useSession(); + + if (status === "loading") return
Loading...
; + if (!session) return
Not authenticated
; + + return ( +
+

Welcome {session.user.firstName}!

+ +
+ ); +} +``` + +### Login/Register +```typescript +// Login +const result = await signIn("credentials", { + email, + password, + redirect: false, +}); + +if (result?.error) { + // Handle error +} + +// Register (via API puis auto-login) +const response = await fetch("/api/auth/register", { + method: "POST", + body: JSON.stringify(data), +}); + +if (response.ok) { + await signIn("credentials", { email, password, redirect: false }); +} + +// Logout +await signOut({ redirect: false }); +``` + +## Compatibilité + +### Utilisateurs existants +✅ **Tous les utilisateurs existants sont compatibles** car: +- La table `users` contient déjà `email` et `password_hash` +- La méthode `userService.verifyCredentials()` vérifie les mots de passe hashés avec bcrypt +- Les UUID sont préservés + +### Sessions +- ⚠️ Les anciennes sessions (cookie `session_token`) ne sont plus valides +- Les utilisateurs devront se reconnecter une fois +- NextAuth utilise ses propres cookies de session + +## Testing + +### Vérifications à faire +1. ✅ Login avec email/password +2. ✅ Logout +3. ✅ Register puis auto-login +4. ✅ Routes protégées redirigent vers /login si non authentifié +5. ✅ /login redirige vers / si déjà authentifié +6. ✅ Les évaluations fonctionnent correctement +7. ✅ Les pages admin fonctionnent correctement + +## Rollback (si nécessaire) + +Si des problèmes surviennent, voici les commits importants: +- Installation de NextAuth +- Création de auth.ts et configuration +- Mise à jour des routes et services +- Suppression de auth-service.ts + +Pour rollback: `git revert ` des commits concernés. + +## Notes + +- NextAuth v5 utilise des cookies signés et sécurisés automatiquement +- Les sessions JWT sont stateless (pas de stockage en base) +- La configuration `authorized` dans auth.ts gère toute la logique de protection des routes +- Le SessionProvider doit wrapper toute l'application pour que `useSession()` fonctionne + diff --git a/app/account/page.tsx b/app/account/page.tsx index fc90953..774616c 100644 --- a/app/account/page.tsx +++ b/app/account/page.tsx @@ -1,18 +1,19 @@ import { redirect } from "next/navigation"; -import { AuthService, userService, TeamsService } from "@/services"; +import { auth } from "@/auth"; +import { userService, TeamsService } from "@/services"; import { AccountForm } from "@/components/account/account-form"; export default async function AccountPage() { try { // Vérifier si l'utilisateur est connecté - const userUuid = await AuthService.getUserUuidFromCookie(); + const session = await auth(); - if (!userUuid) { + if (!session?.user) { redirect("/login"); } // Récupérer le profil utilisateur - const userProfile = await userService.getUserByUuid(userUuid); + const userProfile = await userService.getUserByUuid(session.user.id); if (!userProfile) { redirect("/login"); diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..86c9f3d --- /dev/null +++ b/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,3 @@ +import { handlers } from "@/auth"; + +export const { GET, POST } = handlers; diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts deleted file mode 100644 index 05369ae..0000000 --- a/app/api/auth/login/route.ts +++ /dev/null @@ -1,54 +0,0 @@ -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 } - ); - } - - // 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 } - ); - - // Créer la session et définir le cookie - await AuthService.createSession(user.uuid_id, response); - - 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 deleted file mode 100644 index d1fe202..0000000 --- a/app/api/auth/logout/route.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { NextResponse } from "next/server"; -import { AuthService } from "@/services"; - - -export async function POST() { - try { - // Créer la réponse - const response = NextResponse.json( - { message: "Déconnexion réussie" }, - { status: 200 } - ); - - // Supprimer la session et le cookie - AuthService.removeSession(response); - - 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/profile/route.ts b/app/api/auth/profile/route.ts deleted file mode 100644 index 6a49fa9..0000000 --- a/app/api/auth/profile/route.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { AuthService, userService } from "@/services"; - -export async function PUT(request: NextRequest) { - try { - // Vérifier si l'utilisateur est connecté - const userUuid = await AuthService.getUserUuidFromCookie(); - - if (!userUuid) { - return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); - } - - // Récupérer les données de mise à jour - const { firstName, lastName, teamId } = await request.json(); - - // Validation des données - if (!firstName || !lastName || !teamId) { - return NextResponse.json( - { error: "Tous les champs sont requis" }, - { status: 400 } - ); - } - - // Mettre à jour l'utilisateur - await userService.updateUserByUuid(userUuid, { - firstName, - lastName, - teamId, - }); - - return NextResponse.json({ - message: "Profil mis à jour avec succès", - user: { - firstName, - lastName, - teamId, - }, - }); - } catch (error: any) { - console.error("Profile update error:", error); - return NextResponse.json( - { error: error.message || "Erreur lors de la mise à jour du profil" }, - { status: 500 } - ); - } -} diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index 0f1bd8d..f9ba6c9 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -1,6 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { AuthService, userService } from "@/services"; - +import { userService } from "@/services"; import bcrypt from "bcryptjs"; export async function POST(request: NextRequest) { @@ -45,8 +44,9 @@ export async function POST(request: NextRequest) { ); } - // Créer la réponse avec le cookie de session - const response = NextResponse.json( + // Retourner les informations de l'utilisateur créé + // Le client devra appeler signIn() pour créer la session + return NextResponse.json( { message: "Compte créé avec succès", user: { @@ -59,11 +59,6 @@ export async function POST(request: NextRequest) { }, { status: 201 } ); - - // Créer la session et définir le cookie - await AuthService.createSession(newUser.uuid_id, response); - - return response; } catch (error) { console.error("Register error:", error); return NextResponse.json( diff --git a/app/api/evaluations/skills/route.ts b/app/api/evaluations/skills/route.ts index 54f9199..59980ee 100644 --- a/app/api/evaluations/skills/route.ts +++ b/app/api/evaluations/skills/route.ts @@ -1,11 +1,24 @@ import { NextRequest, NextResponse } from "next/server"; -import { AuthService } from "@/services/auth-service"; +import { auth } from "@/auth"; import { evaluationService } from "@/services/evaluation-service"; export async function PUT(request: NextRequest) { try { - // Récupérer l'utilisateur depuis le cookie (maintenant un UUID) - const { userProfile } = await AuthService.requireAuthenticatedUser(); + // Récupérer l'utilisateur depuis la session NextAuth + const session = await auth(); + + if (!session?.user) { + return NextResponse.json( + { error: "Non authentifié" }, + { status: 401 } + ); + } + + const userProfile = { + firstName: session.user.firstName, + lastName: session.user.lastName, + teamId: session.user.teamId, + }; const body = await request.json(); const { category, skillId, level, canMentor, wantsToLearn, action } = body; diff --git a/app/api/teams/review/route.ts b/app/api/teams/review/route.ts index a1c4ee3..9794b9f 100644 --- a/app/api/teams/review/route.ts +++ b/app/api/teams/review/route.ts @@ -1,13 +1,20 @@ import { NextRequest, NextResponse } from "next/server"; import { TeamReviewService } from "@/services/team-review-service"; -import { AuthService } from "@/services/auth-service"; +import { auth } from "@/auth"; export async function GET(request: NextRequest) { try { // Vérifier l'authentification - const { userProfile } = await AuthService.requireAuthenticatedUser(); + const session = await auth(); + + if (!session?.user) { + return NextResponse.json( + { error: "Non authentifié" }, + { status: 401 } + ); + } - const teamId = userProfile.teamId; + const teamId = session.user.teamId; const data = await TeamReviewService.getTeamReviewData(teamId); return NextResponse.json(data); diff --git a/app/evaluation/page.tsx b/app/evaluation/page.tsx index effb22f..a4cbda9 100644 --- a/app/evaluation/page.tsx +++ b/app/evaluation/page.tsx @@ -1,5 +1,5 @@ import { redirect } from "next/navigation"; -import { AuthService } from "@/services"; +import { auth } from "@/auth"; import { SkillsService, TeamsService } from "@/services"; import { evaluationService } from "@/services/evaluation-service"; import { EvaluationClientWrapper } from "@/components/evaluation"; @@ -7,12 +7,14 @@ import { SkillEvaluation } from "@/components/skill-evaluation"; export default async function EvaluationPage() { // Charger les données côté serveur - const userUuid = await AuthService.getUserUuidFromCookie(); + const session = await auth(); - if (!userUuid) { + if (!session?.user) { redirect("/login"); } + const userUuid = session.user.id; + const [userEvaluation, skillCategories, teams] = await Promise.all([ evaluationService.getServerUserEvaluation(userUuid!), SkillsService.getSkillCategories(), diff --git a/app/layout.tsx b/app/layout.tsx index 9fdbdac..dd84ec5 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -7,7 +7,9 @@ import { ThemeProvider } from "@/components/layout/theme-provider"; import { Toaster } from "@/components/ui/sonner"; import { UserProvider } from "@/hooks/use-user-context"; import { NavigationWrapper } from "@/components/layout/navigation-wrapper"; -import { AuthService, TeamsService } from "@/services"; +import { SessionProvider } from "@/components/auth/session-provider"; +import { auth } from "@/auth"; +import { TeamsService } from "@/services"; export const metadata: Metadata = { title: "PeakSkills - Auto-évaluation de compétences", @@ -20,32 +22,33 @@ export default async function RootLayout({ }: { children: React.ReactNode; }) { - // Récupérer les infos utilisateur côté serveur + // Récupérer les infos utilisateur depuis la session NextAuth let userInfo = null; try { - const { userUuid, userProfile } = - await AuthService.requireAuthenticatedUser(); + const session = await auth(); - // Récupérer le nom de l'équipe - let teamName = "Équipe non définie"; - if (userProfile.teamId) { - try { - const team = await TeamsService.getTeamById(userProfile.teamId); - if (team) { - teamName = team.name; + if (session?.user) { + // Récupérer le nom de l'équipe + let teamName = "Équipe non définie"; + if (session.user.teamId) { + try { + const team = await TeamsService.getTeamById(session.user.teamId); + if (team) { + teamName = team.name; + } + } catch (error) { + console.error("Failed to fetch team name:", error); } - } catch (error) { - console.error("Failed to fetch team name:", error); } - } - userInfo = { - firstName: userProfile.firstName, - lastName: userProfile.lastName, - teamName, - teamId: userProfile.teamId, - uuid: userUuid, - }; + userInfo = { + firstName: session.user.firstName, + lastName: session.user.lastName, + teamName, + teamId: session.user.teamId, + uuid: session.user.id, + }; + } } catch (error) { // Utilisateur non authentifié, userInfo reste null console.log("User not authenticated:", error); @@ -56,18 +59,20 @@ export default async function RootLayout({ - - - -
{children}
- -
-
+ + + + +
{children}
+ +
+
+
); diff --git a/app/login/page.tsx b/app/login/page.tsx index 435697c..056ff39 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,5 +1,5 @@ import { TeamsService, userService } from "@/services"; -import { AuthService } from "@/services"; +import { auth } from "@/auth"; import { LoginLayout, AuthWrapper, LoginLoading } from "@/components/login"; export default async function LoginPage() { @@ -8,13 +8,13 @@ export default async function LoginPage() { const teams = await TeamsService.getTeams(); // Vérifier si l'utilisateur est déjà connecté - const userUuid = await AuthService.getUserUuidFromCookie(); + const session = await auth(); let userProfile = null; - if (userUuid) { + if (session?.user) { // Si l'utilisateur est connecté, récupérer son profil côté serveur - userProfile = await userService.getUserByUuid(userUuid); + userProfile = await userService.getUserByUuid(session.user.id); } // Si l'utilisateur n'est pas connecté, afficher le formulaire d'auth diff --git a/app/page.tsx b/app/page.tsx index 536b80d..dcc81b6 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,5 +1,5 @@ import { redirect } from "next/navigation"; -import { AuthService } from "@/services"; +import { auth } from "@/auth"; import { evaluationService, SkillsService, TeamsService } from "@/services"; import { generateRadarData } from "@/lib/evaluation-utils"; import { @@ -14,12 +14,14 @@ import { export default async function HomePage() { // Charger les données côté serveur - const userUuid = await AuthService.getUserUuidFromCookie(); + const session = await auth(); - if (!userUuid) { + if (!session?.user) { redirect("/login"); } + const userUuid = session.user.id; + const [userEvaluation, skillCategories, teams] = await Promise.all([ evaluationService.getServerUserEvaluation(userUuid!), SkillsService.getSkillCategories(), diff --git a/app/team/page.tsx b/app/team/page.tsx index c25b8c5..6414d2f 100644 --- a/app/team/page.tsx +++ b/app/team/page.tsx @@ -1,5 +1,5 @@ import { TeamReviewService } from "@/services/team-review-service"; -import { AuthService } from "@/services/auth-service"; +import { auth } from "@/auth"; import { redirect } from "next/navigation"; import { TeamOverview } from "@/components/team-review/team-overview"; import { SkillMatrix } from "@/components/team-review/skill-matrix"; @@ -13,10 +13,14 @@ export const dynamic = "force-dynamic"; async function TeamReviewPage() { try { - const { userProfile } = await AuthService.requireAuthenticatedUser(); + const session = await auth(); + + if (!session?.user) { + redirect("/login"); + } const teamData = await TeamReviewService.getTeamReviewData( - userProfile.teamId + session.user.teamId ); return ( diff --git a/auth.ts b/auth.ts new file mode 100644 index 0000000..14a77d9 --- /dev/null +++ b/auth.ts @@ -0,0 +1,69 @@ +import NextAuth from "next-auth"; +import Credentials from "next-auth/providers/credentials"; + +export const { handlers, signIn, signOut, auth } = NextAuth({ + providers: [ + Credentials({ + credentials: { + email: { label: "Email", type: "email" }, + password: { label: "Password", type: "password" }, + }, + async authorize(credentials) { + if (!credentials?.email || !credentials?.password) { + return null; + } + + // Import dynamique pour éviter l'erreur Edge Runtime dans le middleware + const { userService } = await import("@/services/user-service"); + + const user = await userService.verifyCredentials( + credentials.email as string, + credentials.password as string + ); + + if (!user) { + return null; + } + + // Retourner l'utilisateur au format attendu par NextAuth + return { + id: user.uuid_id, + email: user.email, + name: `${user.first_name} ${user.last_name}`, + firstName: user.first_name, + lastName: user.last_name, + teamId: user.team_id, + }; + }, + }), + ], + pages: { + signIn: "/login", + }, + session: { + strategy: "jwt", + }, + callbacks: { + async jwt({ token, user }) { + // Lors de la première connexion, ajouter les infos custom au token + if (user) { + token.id = user.id; + token.teamId = (user as any).teamId; + token.firstName = (user as any).firstName; + token.lastName = (user as any).lastName; + } + return token; + }, + async session({ session, token }) { + // Ajouter les infos du token à la session + if (token && session.user) { + session.user.id = token.id as string; + session.user.teamId = token.teamId as string; + session.user.firstName = token.firstName as string; + session.user.lastName = token.lastName as string; + } + return session; + }, + }, +}); + diff --git a/clients/domains/auth-client.ts b/clients/domains/auth-client.ts index ae17752..f755b8d 100644 --- a/clients/domains/auth-client.ts +++ b/clients/domains/auth-client.ts @@ -1,5 +1,5 @@ +import { signIn, signOut } from "next-auth/react"; import { BaseHttpClient } from "../base/http-client"; -import { UserProfile } from "../../lib/types"; export interface LoginCredentials { email: string; @@ -24,27 +24,54 @@ export interface AuthUser { export class AuthClient extends BaseHttpClient { /** - * Connecte un utilisateur avec email/password + * Connecte un utilisateur avec email/password via NextAuth */ - async login( - credentials: LoginCredentials - ): Promise<{ user: AuthUser; message: string }> { - return await this.post("/auth/login", credentials); + async login(credentials: LoginCredentials): Promise<{ success: boolean; error?: string }> { + const result = await signIn("credentials", { + email: credentials.email, + password: credentials.password, + redirect: false, + }); + + if (result?.error) { + return { success: false, error: "Email ou mot de passe incorrect" }; + } + + return { success: true }; } /** - * Crée un nouveau compte utilisateur + * Crée un nouveau compte utilisateur puis se connecte automatiquement */ - async register( - data: RegisterData - ): Promise<{ user: AuthUser; message: string }> { - return await this.post("/auth/register", data); + async register(data: RegisterData): Promise<{ success: boolean; error?: string }> { + try { + // Créer l'utilisateur via l'API + const response = await this.post<{ user: AuthUser; message: string }>("/auth/register", data); + + // Se connecter automatiquement après création + const loginResult = await signIn("credentials", { + email: data.email, + password: data.password, + redirect: false, + }); + + if (loginResult?.error) { + return { success: false, error: "Compte créé mais erreur de connexion" }; + } + + return { success: true }; + } catch (error: any) { + return { + success: false, + error: error.message || "Erreur lors de la création du compte" + }; + } } /** - * Déconnecte l'utilisateur + * Déconnecte l'utilisateur via NextAuth */ - async logout(): Promise<{ message: string }> { - return await this.post("/auth/logout"); + async logout(): Promise { + await signOut({ redirect: false }); } } diff --git a/components/auth/session-provider.tsx b/components/auth/session-provider.tsx new file mode 100644 index 0000000..16522b3 --- /dev/null +++ b/components/auth/session-provider.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { SessionProvider as NextAuthSessionProvider } from "next-auth/react"; +import { ReactNode } from "react"; + +interface SessionProviderProps { + children: ReactNode; +} + +export function SessionProvider({ children }: SessionProviderProps) { + return {children}; +} + diff --git a/components/login/auth-wrapper.tsx b/components/login/auth-wrapper.tsx index f6299f0..349c3e8 100644 --- a/components/login/auth-wrapper.tsx +++ b/components/login/auth-wrapper.tsx @@ -31,19 +31,30 @@ export function AuthWrapper({ teams, initialUser }: AuthWrapperProps) { setLoading(true); setError(null); try { - const response = await authClient.login({ email, password }); + const result = await authClient.login({ email, password }); + + if (!result.success) { + setError(result.error || "Erreur de connexion"); + toast({ + title: "Erreur de connexion", + description: result.error || "Erreur de connexion", + variant: "destructive", + }); + return; + } toast({ title: "Connexion réussie", - description: response.message, + description: "Bienvenue !", }); // Rediriger vers l'accueil après connexion router.push("/"); + router.refresh(); } catch (error: any) { console.error("Login failed:", error); - const errorMessage = error.response?.data?.error || "Erreur de connexion"; + const errorMessage = error.message || "Erreur de connexion"; setError(errorMessage); toast({ @@ -66,26 +77,30 @@ export function AuthWrapper({ teams, initialUser }: AuthWrapperProps) { setLoading(true); setError(null); try { - const response = await authClient.register(data); + const result = await authClient.register(data); + + if (!result.success) { + setError(result.error || "Erreur lors de la création du compte"); + toast({ + title: "Erreur d'inscription", + description: result.error || "Erreur lors de la création du compte", + variant: "destructive", + }); + return; + } toast({ title: "Compte créé", - description: response.message, - }); - - // Après inscription réussie, connecter automatiquement l'utilisateur - toast({ - title: "Connexion automatique", - description: "Redirection vers l'accueil...", + description: "Connexion automatique...", }); // Rediriger vers l'accueil après inscription réussie router.push("/"); + router.refresh(); } catch (error: any) { console.error("Register failed:", error); - const errorMessage = - error.response?.data?.error || "Erreur lors de la création du compte"; + const errorMessage = error.message || "Erreur lors de la création du compte"; setError(errorMessage); toast({ @@ -102,11 +117,13 @@ export function AuthWrapper({ teams, initialUser }: AuthWrapperProps) { try { await authClient.logout(); // Rediriger vers la page de login après déconnexion - window.location.href = "/login"; + router.push("/login"); + router.refresh(); } catch (error) { console.error("Logout failed:", error); // En cas d'erreur, forcer le rechargement pour nettoyer l'état - window.location.reload(); + router.push("/login"); + router.refresh(); } }; diff --git a/middleware.ts b/middleware.ts index 0165dff..5895bc0 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,44 +1,28 @@ -import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@/auth"; +import { NextResponse } from "next/server"; -const COOKIE_NAME = "session_token"; +export default auth((req) => { + const { pathname } = req.nextUrl; + const isLoggedIn = !!req.auth; + const isOnLoginPage = pathname === "/login"; -export function middleware(request: NextRequest) { - const { pathname } = request.nextUrl; - - // Pages qui ne nécessitent pas d'authentification - const publicPaths = ["/login"]; - - // Pages API qui ne nécessitent pas d'authentification - const publicApiPaths = ["/api/teams", "/api/auth"]; - console.log(pathname); - - // Vérifier si c'est une route publique - if ( - publicPaths.includes(pathname) || - publicApiPaths.some((path) => pathname.startsWith(path)) - ) { + // Pages publiques (API auth et teams) + if (pathname.startsWith("/api/auth") || pathname.startsWith("/api/teams")) { return NextResponse.next(); } - // Vérifier si c'est un fichier statique - if ( - pathname.includes("/_next/") || - pathname.includes("/favicon.ico") || - pathname.includes("/public/") - ) { - return NextResponse.next(); + // Si connecté et sur login, rediriger vers home + if (isLoggedIn && isOnLoginPage) { + return NextResponse.redirect(new URL("/", req.url)); } - // Vérifier le cookie d'authentification (maintenant un UUID) - const userUuid = request.cookies.get(COOKIE_NAME)?.value; - if (!userUuid) { - // Rediriger vers la page de login si pas authentifié - const loginUrl = new URL("/login", request.url); - return NextResponse.redirect(loginUrl); + // Si non connecté et pas sur login, rediriger vers login + if (!isLoggedIn && !isOnLoginPage) { + return NextResponse.redirect(new URL("/login", req.url)); } return NextResponse.next(); -} +}); export const config = { matcher: [ diff --git a/package.json b/package.json index 7afc943..f6907db 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "input-otp": "1.4.1", "lucide-react": "^0.454.0", "next": "15.2.4", + "next-auth": "5.0.0-beta.29", "next-themes": "^0.4.6", "pg": "^8.12.0", "react": "^19", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 675059c..7f95300 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -149,6 +149,9 @@ importers: next: specifier: 15.2.4 version: 15.2.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + next-auth: + specifier: 5.0.0-beta.29 + version: 5.0.0-beta.29(next@15.2.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -223,6 +226,20 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@auth/core@0.40.0': + resolution: {integrity: sha512-n53uJE0RH5SqZ7N1xZoMKekbHfQgjd0sAEyUbE+IYJnmuQkbvuZnXItCU7d+i7Fj8VGOgqvNO7Mw4YfBTlZeQw==} + peerDependencies: + '@simplewebauthn/browser': ^9.0.1 + '@simplewebauthn/server': ^9.0.2 + nodemailer: ^6.8.0 + peerDependenciesMeta: + '@simplewebauthn/browser': + optional: true + '@simplewebauthn/server': + optional: true + nodemailer: + optional: true + '@babel/runtime@7.28.3': resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==} engines: {node: '>=6.9.0'} @@ -607,6 +624,9 @@ packages: cpu: [x64] os: [win32] + '@panva/hkdf@1.2.1': + resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==} + '@radix-ui/number@1.1.0': resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} @@ -1612,6 +1632,9 @@ packages: resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} hasBin: true + jose@6.1.0: + resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1712,6 +1735,22 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + next-auth@5.0.0-beta.29: + resolution: {integrity: sha512-Ukpnuk3NMc/LiOl32njZPySk7pABEzbjhMUFd5/n10I0ZNC7NCuVv8IY2JgbDek2t/PUOifQEoUiOOTLy4os5A==} + peerDependencies: + '@simplewebauthn/browser': ^9.0.1 + '@simplewebauthn/server': ^9.0.2 + next: ^14.0.0-0 || ^15.0.0-0 + nodemailer: ^6.6.5 + react: ^18.2.0 || ^19.0.0-0 + peerDependenciesMeta: + '@simplewebauthn/browser': + optional: true + '@simplewebauthn/server': + optional: true + nodemailer: + optional: true + next-themes@0.4.6: resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} peerDependencies: @@ -1754,6 +1793,9 @@ packages: resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} engines: {node: '>=0.10.0'} + oauth4webapi@3.8.2: + resolution: {integrity: sha512-FzZZ+bht5X0FKe7Mwz3DAVAmlH1BV5blSak/lHMBKz0/EBMhX6B10GlQYI51+oRp8ObJaX0g6pXrAxZh5s8rjw==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1822,6 +1864,14 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} + preact-render-to-string@6.5.11: + resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==} + peerDependencies: + preact: '>=10' + + preact@10.24.3: + resolution: {integrity: sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -2055,6 +2105,14 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@auth/core@0.40.0': + dependencies: + '@panva/hkdf': 1.2.1 + jose: 6.1.0 + oauth4webapi: 3.8.2 + preact: 10.24.3 + preact-render-to-string: 6.5.11(preact@10.24.3) + '@babel/runtime@7.28.3': {} '@date-fns/tz@1.2.0': {} @@ -2307,6 +2365,8 @@ snapshots: '@next/swc-win32-x64-msvc@15.2.4': optional: true + '@panva/hkdf@1.2.1': {} + '@radix-ui/number@1.1.0': {} '@radix-ui/primitive@1.1.1': {} @@ -3341,6 +3401,8 @@ snapshots: jiti@2.5.1: {} + jose@6.1.0: {} + js-tokens@4.0.0: {} lightningcss-darwin-arm64@1.30.1: @@ -3412,6 +3474,12 @@ snapshots: nanoid@3.3.11: {} + next-auth@5.0.0-beta.29(next@15.2.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1): + dependencies: + '@auth/core': 0.40.0 + next: 15.2.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + react: 19.1.1 + next-themes@0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: react: 19.1.1 @@ -3450,6 +3518,8 @@ snapshots: normalize-range@0.1.2: {} + oauth4webapi@3.8.2: {} + object-assign@4.1.1: {} pg-cloudflare@1.2.7: @@ -3513,6 +3583,12 @@ snapshots: dependencies: xtend: 4.0.2 + preact-render-to-string@6.5.11(preact@10.24.3): + dependencies: + preact: 10.24.3 + + preact@10.24.3: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 diff --git a/services/auth-service.ts b/services/auth-service.ts deleted file mode 100644 index 32ebd0f..0000000 --- a/services/auth-service.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { cookies } from "next/headers"; -import { NextResponse } from "next/server"; -import { UserProfile } from "@/lib/types"; -import { userService } from "@/services/user-service"; - -export const COOKIE_NAME = "session_token" as const; - -export const COOKIE_CONFIG = { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "lax" as const, - maxAge: 60 * 60 * 24 * 7, // 7 jours - path: "/", -} as const; - -/** - * Service d'authentification côté serveur - * Implémente les méthodes qui nécessitent next/headers - */ -export class AuthService { - /** - * Récupère l'UUID utilisateur depuis le cookie côté serveur - */ - static async getUserUuidFromCookie(): Promise { - const cookieStore = await cookies(); - const userUuidCookie = cookieStore.get(COOKIE_NAME); - return userUuidCookie?.value || null; - } - - /** - * Vérifie si l'utilisateur est authentifié côté serveur - */ - static async isUserAuthenticated(): Promise { - const userUuid = await this.getUserUuidFromCookie(); - return !!userUuid; - } - - /** - * Vérifie l'authentification et retourne le profil utilisateur - * @throws {Error} avec status 401 si non authentifié ou 404 si utilisateur non trouvé - */ - static async requireAuthenticatedUser(): Promise<{ - userUuid: string; - userProfile: UserProfile; - }> { - const userUuid = await this.getUserUuidFromCookie(); - - if (!userUuid) { - const error = new Error("Utilisateur non authentifié"); - (error as any).status = 401; - throw error; - } - - const userProfile = await userService.getUserByUuid(userUuid); - if (!userProfile) { - const error = new Error("Utilisateur introuvable"); - (error as any).status = 404; - throw error; - } - - return { userUuid, userProfile }; - } - - - - /** - * Crée une nouvelle session pour un utilisateur et retourne la réponse avec le cookie - */ - static async createSession(userUuid: string, response: NextResponse): Promise { - // 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 - const sessionToken = userUuid; - response.cookies.set(COOKIE_NAME, sessionToken, COOKIE_CONFIG); - return response; - } - - /** - * Supprime la session et retourne la réponse avec le cookie expiré - */ - static removeSession(response: NextResponse): NextResponse { - response.cookies.set(COOKIE_NAME, "", { ...COOKIE_CONFIG, maxAge: 0 }); - return response; - } - - /** - * Valide un token de session et retourne l'UUID utilisateur - */ - static async validateSession(sessionToken: string): Promise { - // 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; - } -} diff --git a/services/evaluation-service.ts b/services/evaluation-service.ts index 131ec2a..2e112f1 100644 --- a/services/evaluation-service.ts +++ b/services/evaluation-service.ts @@ -1,6 +1,5 @@ import { getPool } from "./database"; import { userService } from "./user-service"; -import { AuthService } from "./auth-service"; import { UserEvaluation, UserProfile, @@ -121,8 +120,12 @@ export class EvaluationService { try { await client.query("BEGIN"); - // 1. Upsert user avec UUID - const userUuid = await AuthService.getUserUuidFromCookie(); + // 1. Récupérer le userUuid depuis le profile de l'évaluation + const existingUser = await userService.findUserByProfile(evaluation.profile); + if (!existingUser) { + throw new Error("Utilisateur non trouvé"); + } + const userUuid = existingUser.uuid; // 2. Upsert user_evaluation avec user_uuid const userEvalQuery = ` @@ -624,7 +627,12 @@ export class EvaluationService { try { await client.query("BEGIN"); - const userUuid = await AuthService.getUserUuidFromCookie(); + // Récupérer le userUuid depuis le profile + const existingUser = await userService.findUserByProfile(profile); + if (!existingUser) { + throw new Error("Utilisateur non trouvé"); + } + const userUuid = existingUser.uuid; // Supprimer directement la skill evaluation const deleteQuery = ` diff --git a/services/index.ts b/services/index.ts index a1988ec..dad9864 100644 --- a/services/index.ts +++ b/services/index.ts @@ -22,6 +22,3 @@ export { AdminService } from "./admin-service"; // Admin types (can be imported anywhere) export type { TeamMember, TeamStats, DirectionStats } from "@/lib/admin-types"; - -// Server auth service (server-side only) -export { AuthService, COOKIE_NAME, COOKIE_MAX_AGE } from "./auth-service"; diff --git a/types/next-auth.d.ts b/types/next-auth.d.ts new file mode 100644 index 0000000..f82b1c3 --- /dev/null +++ b/types/next-auth.d.ts @@ -0,0 +1,28 @@ +import { DefaultSession } from "next-auth"; + +declare module "next-auth" { + interface Session { + user: { + id: string; + teamId: string; + firstName: string; + lastName: string; + } & DefaultSession["user"]; + } + + interface User { + teamId: string; + firstName: string; + lastName: string; + } +} + +declare module "next-auth/jwt" { + interface JWT { + id: string; + teamId: string; + firstName: string; + lastName: string; + } +} +