refactor: migrate authentication to NextAuth and clean up related services
This commit is contained in:
174
NEXTAUTH_MIGRATION.md
Normal file
174
NEXTAUTH_MIGRATION.md
Normal file
@@ -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 `<SessionProvider>` 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 <div>Loading...</div>;
|
||||||
|
if (!session) return <div>Not authenticated</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>Welcome {session.user.firstName}!</p>
|
||||||
|
<button onClick={() => signOut()}>Logout</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 <commit-hash>` 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
|
||||||
|
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
import { redirect } from "next/navigation";
|
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";
|
import { AccountForm } from "@/components/account/account-form";
|
||||||
|
|
||||||
export default async function AccountPage() {
|
export default async function AccountPage() {
|
||||||
try {
|
try {
|
||||||
// Vérifier si l'utilisateur est connecté
|
// Vérifier si l'utilisateur est connecté
|
||||||
const userUuid = await AuthService.getUserUuidFromCookie();
|
const session = await auth();
|
||||||
|
|
||||||
if (!userUuid) {
|
if (!session?.user) {
|
||||||
redirect("/login");
|
redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Récupérer le profil utilisateur
|
// Récupérer le profil utilisateur
|
||||||
const userProfile = await userService.getUserByUuid(userUuid);
|
const userProfile = await userService.getUserByUuid(session.user.id);
|
||||||
|
|
||||||
if (!userProfile) {
|
if (!userProfile) {
|
||||||
redirect("/login");
|
redirect("/login");
|
||||||
|
|||||||
3
app/api/auth/[...nextauth]/route.ts
Normal file
3
app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { handlers } from "@/auth";
|
||||||
|
|
||||||
|
export const { GET, POST } = handlers;
|
||||||
@@ -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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { AuthService, userService } from "@/services";
|
import { userService } from "@/services";
|
||||||
|
|
||||||
import bcrypt from "bcryptjs";
|
import bcrypt from "bcryptjs";
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
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
|
// Retourner les informations de l'utilisateur créé
|
||||||
const response = NextResponse.json(
|
// Le client devra appeler signIn() pour créer la session
|
||||||
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
message: "Compte créé avec succès",
|
message: "Compte créé avec succès",
|
||||||
user: {
|
user: {
|
||||||
@@ -59,11 +59,6 @@ export async function POST(request: NextRequest) {
|
|||||||
},
|
},
|
||||||
{ status: 201 }
|
{ status: 201 }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Créer la session et définir le cookie
|
|
||||||
await AuthService.createSession(newUser.uuid_id, response);
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Register error:", error);
|
console.error("Register error:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -1,11 +1,24 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { AuthService } from "@/services/auth-service";
|
import { auth } from "@/auth";
|
||||||
import { evaluationService } from "@/services/evaluation-service";
|
import { evaluationService } from "@/services/evaluation-service";
|
||||||
|
|
||||||
export async function PUT(request: NextRequest) {
|
export async function PUT(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
// Récupérer l'utilisateur depuis le cookie (maintenant un UUID)
|
// Récupérer l'utilisateur depuis la session NextAuth
|
||||||
const { userProfile } = await AuthService.requireAuthenticatedUser();
|
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 body = await request.json();
|
||||||
const { category, skillId, level, canMentor, wantsToLearn, action } = body;
|
const { category, skillId, level, canMentor, wantsToLearn, action } = body;
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { TeamReviewService } from "@/services/team-review-service";
|
import { TeamReviewService } from "@/services/team-review-service";
|
||||||
import { AuthService } from "@/services/auth-service";
|
import { auth } from "@/auth";
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
// Vérifier l'authentification
|
// 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);
|
const data = await TeamReviewService.getTeamReviewData(teamId);
|
||||||
|
|
||||||
return NextResponse.json(data);
|
return NextResponse.json(data);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { AuthService } from "@/services";
|
import { auth } from "@/auth";
|
||||||
import { SkillsService, TeamsService } from "@/services";
|
import { SkillsService, TeamsService } from "@/services";
|
||||||
import { evaluationService } from "@/services/evaluation-service";
|
import { evaluationService } from "@/services/evaluation-service";
|
||||||
import { EvaluationClientWrapper } from "@/components/evaluation";
|
import { EvaluationClientWrapper } from "@/components/evaluation";
|
||||||
@@ -7,12 +7,14 @@ import { SkillEvaluation } from "@/components/skill-evaluation";
|
|||||||
|
|
||||||
export default async function EvaluationPage() {
|
export default async function EvaluationPage() {
|
||||||
// Charger les données côté serveur
|
// Charger les données côté serveur
|
||||||
const userUuid = await AuthService.getUserUuidFromCookie();
|
const session = await auth();
|
||||||
|
|
||||||
if (!userUuid) {
|
if (!session?.user) {
|
||||||
redirect("/login");
|
redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userUuid = session.user.id;
|
||||||
|
|
||||||
const [userEvaluation, skillCategories, teams] = await Promise.all([
|
const [userEvaluation, skillCategories, teams] = await Promise.all([
|
||||||
evaluationService.getServerUserEvaluation(userUuid!),
|
evaluationService.getServerUserEvaluation(userUuid!),
|
||||||
SkillsService.getSkillCategories(),
|
SkillsService.getSkillCategories(),
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import { ThemeProvider } from "@/components/layout/theme-provider";
|
|||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { UserProvider } from "@/hooks/use-user-context";
|
import { UserProvider } from "@/hooks/use-user-context";
|
||||||
import { NavigationWrapper } from "@/components/layout/navigation-wrapper";
|
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 = {
|
export const metadata: Metadata = {
|
||||||
title: "PeakSkills - Auto-évaluation de compétences",
|
title: "PeakSkills - Auto-évaluation de compétences",
|
||||||
@@ -20,32 +22,33 @@ export default async function RootLayout({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
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;
|
let userInfo = null;
|
||||||
try {
|
try {
|
||||||
const { userUuid, userProfile } =
|
const session = await auth();
|
||||||
await AuthService.requireAuthenticatedUser();
|
|
||||||
|
|
||||||
// Récupérer le nom de l'équipe
|
if (session?.user) {
|
||||||
let teamName = "Équipe non définie";
|
// Récupérer le nom de l'équipe
|
||||||
if (userProfile.teamId) {
|
let teamName = "Équipe non définie";
|
||||||
try {
|
if (session.user.teamId) {
|
||||||
const team = await TeamsService.getTeamById(userProfile.teamId);
|
try {
|
||||||
if (team) {
|
const team = await TeamsService.getTeamById(session.user.teamId);
|
||||||
teamName = team.name;
|
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 = {
|
userInfo = {
|
||||||
firstName: userProfile.firstName,
|
firstName: session.user.firstName,
|
||||||
lastName: userProfile.lastName,
|
lastName: session.user.lastName,
|
||||||
teamName,
|
teamName,
|
||||||
teamId: userProfile.teamId,
|
teamId: session.user.teamId,
|
||||||
uuid: userUuid,
|
uuid: session.user.id,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Utilisateur non authentifié, userInfo reste null
|
// Utilisateur non authentifié, userInfo reste null
|
||||||
console.log("User not authenticated:", error);
|
console.log("User not authenticated:", error);
|
||||||
@@ -56,18 +59,20 @@ export default async function RootLayout({
|
|||||||
<body
|
<body
|
||||||
className={`${GeistSans.variable} ${GeistMono.variable} antialiased`}
|
className={`${GeistSans.variable} ${GeistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
<ThemeProvider
|
<SessionProvider>
|
||||||
attribute="class"
|
<ThemeProvider
|
||||||
defaultTheme="system"
|
attribute="class"
|
||||||
enableSystem
|
defaultTheme="system"
|
||||||
disableTransitionOnChange
|
enableSystem
|
||||||
>
|
disableTransitionOnChange
|
||||||
<UserProvider initialUserInfo={userInfo}>
|
>
|
||||||
<NavigationWrapper />
|
<UserProvider initialUserInfo={userInfo}>
|
||||||
<main className="min-h-screen">{children}</main>
|
<NavigationWrapper />
|
||||||
<Toaster />
|
<main className="min-h-screen">{children}</main>
|
||||||
</UserProvider>
|
<Toaster />
|
||||||
</ThemeProvider>
|
</UserProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</SessionProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TeamsService, userService } from "@/services";
|
import { TeamsService, userService } from "@/services";
|
||||||
import { AuthService } from "@/services";
|
import { auth } from "@/auth";
|
||||||
import { LoginLayout, AuthWrapper, LoginLoading } from "@/components/login";
|
import { LoginLayout, AuthWrapper, LoginLoading } from "@/components/login";
|
||||||
|
|
||||||
export default async function LoginPage() {
|
export default async function LoginPage() {
|
||||||
@@ -8,13 +8,13 @@ export default async function LoginPage() {
|
|||||||
const teams = await TeamsService.getTeams();
|
const teams = await TeamsService.getTeams();
|
||||||
|
|
||||||
// Vérifier si l'utilisateur est déjà connecté
|
// Vérifier si l'utilisateur est déjà connecté
|
||||||
const userUuid = await AuthService.getUserUuidFromCookie();
|
const session = await auth();
|
||||||
|
|
||||||
let userProfile = null;
|
let userProfile = null;
|
||||||
|
|
||||||
if (userUuid) {
|
if (session?.user) {
|
||||||
// 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
|
||||||
userProfile = await userService.getUserByUuid(userUuid);
|
userProfile = await userService.getUserByUuid(session.user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si l'utilisateur n'est pas connecté, afficher le formulaire d'auth
|
// Si l'utilisateur n'est pas connecté, afficher le formulaire d'auth
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { AuthService } from "@/services";
|
import { auth } from "@/auth";
|
||||||
import { evaluationService, SkillsService, TeamsService } from "@/services";
|
import { evaluationService, SkillsService, TeamsService } from "@/services";
|
||||||
import { generateRadarData } from "@/lib/evaluation-utils";
|
import { generateRadarData } from "@/lib/evaluation-utils";
|
||||||
import {
|
import {
|
||||||
@@ -14,12 +14,14 @@ import {
|
|||||||
|
|
||||||
export default async function HomePage() {
|
export default async function HomePage() {
|
||||||
// Charger les données côté serveur
|
// Charger les données côté serveur
|
||||||
const userUuid = await AuthService.getUserUuidFromCookie();
|
const session = await auth();
|
||||||
|
|
||||||
if (!userUuid) {
|
if (!session?.user) {
|
||||||
redirect("/login");
|
redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userUuid = session.user.id;
|
||||||
|
|
||||||
const [userEvaluation, skillCategories, teams] = await Promise.all([
|
const [userEvaluation, skillCategories, teams] = await Promise.all([
|
||||||
evaluationService.getServerUserEvaluation(userUuid!),
|
evaluationService.getServerUserEvaluation(userUuid!),
|
||||||
SkillsService.getSkillCategories(),
|
SkillsService.getSkillCategories(),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TeamReviewService } from "@/services/team-review-service";
|
import { TeamReviewService } from "@/services/team-review-service";
|
||||||
import { AuthService } from "@/services/auth-service";
|
import { auth } from "@/auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { TeamOverview } from "@/components/team-review/team-overview";
|
import { TeamOverview } from "@/components/team-review/team-overview";
|
||||||
import { SkillMatrix } from "@/components/team-review/skill-matrix";
|
import { SkillMatrix } from "@/components/team-review/skill-matrix";
|
||||||
@@ -13,10 +13,14 @@ export const dynamic = "force-dynamic";
|
|||||||
|
|
||||||
async function TeamReviewPage() {
|
async function TeamReviewPage() {
|
||||||
try {
|
try {
|
||||||
const { userProfile } = await AuthService.requireAuthenticatedUser();
|
const session = await auth();
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
redirect("/login");
|
||||||
|
}
|
||||||
|
|
||||||
const teamData = await TeamReviewService.getTeamReviewData(
|
const teamData = await TeamReviewService.getTeamReviewData(
|
||||||
userProfile.teamId
|
session.user.teamId
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
69
auth.ts
Normal file
69
auth.ts
Normal file
@@ -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;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { signIn, signOut } from "next-auth/react";
|
||||||
import { BaseHttpClient } from "../base/http-client";
|
import { BaseHttpClient } from "../base/http-client";
|
||||||
import { UserProfile } from "../../lib/types";
|
|
||||||
|
|
||||||
export interface LoginCredentials {
|
export interface LoginCredentials {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -24,27 +24,54 @@ export interface AuthUser {
|
|||||||
|
|
||||||
export class AuthClient extends BaseHttpClient {
|
export class AuthClient extends BaseHttpClient {
|
||||||
/**
|
/**
|
||||||
* Connecte un utilisateur avec email/password
|
* Connecte un utilisateur avec email/password via NextAuth
|
||||||
*/
|
*/
|
||||||
async login(
|
async login(credentials: LoginCredentials): Promise<{ success: boolean; error?: string }> {
|
||||||
credentials: LoginCredentials
|
const result = await signIn("credentials", {
|
||||||
): Promise<{ user: AuthUser; message: string }> {
|
email: credentials.email,
|
||||||
return await this.post("/auth/login", credentials);
|
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(
|
async register(data: RegisterData): Promise<{ success: boolean; error?: string }> {
|
||||||
data: RegisterData
|
try {
|
||||||
): Promise<{ user: AuthUser; message: string }> {
|
// Créer l'utilisateur via l'API
|
||||||
return await this.post("/auth/register", data);
|
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 }> {
|
async logout(): Promise<void> {
|
||||||
return await this.post("/auth/logout");
|
await signOut({ redirect: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
components/auth/session-provider.tsx
Normal file
13
components/auth/session-provider.tsx
Normal file
@@ -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 <NextAuthSessionProvider>{children}</NextAuthSessionProvider>;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -31,19 +31,30 @@ export function AuthWrapper({ teams, initialUser }: AuthWrapperProps) {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
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({
|
toast({
|
||||||
title: "Connexion réussie",
|
title: "Connexion réussie",
|
||||||
description: response.message,
|
description: "Bienvenue !",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Rediriger vers l'accueil après connexion
|
// Rediriger vers l'accueil après connexion
|
||||||
router.push("/");
|
router.push("/");
|
||||||
|
router.refresh();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Login failed:", error);
|
console.error("Login failed:", error);
|
||||||
|
|
||||||
const errorMessage = error.response?.data?.error || "Erreur de connexion";
|
const errorMessage = error.message || "Erreur de connexion";
|
||||||
|
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
toast({
|
toast({
|
||||||
@@ -66,26 +77,30 @@ export function AuthWrapper({ teams, initialUser }: AuthWrapperProps) {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
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({
|
toast({
|
||||||
title: "Compte créé",
|
title: "Compte créé",
|
||||||
description: response.message,
|
description: "Connexion automatique...",
|
||||||
});
|
|
||||||
|
|
||||||
// Après inscription réussie, connecter automatiquement l'utilisateur
|
|
||||||
toast({
|
|
||||||
title: "Connexion automatique",
|
|
||||||
description: "Redirection vers l'accueil...",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Rediriger vers l'accueil après inscription réussie
|
// Rediriger vers l'accueil après inscription réussie
|
||||||
router.push("/");
|
router.push("/");
|
||||||
|
router.refresh();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Register failed:", error);
|
console.error("Register failed:", error);
|
||||||
|
|
||||||
const errorMessage =
|
const errorMessage = error.message || "Erreur lors de la création du compte";
|
||||||
error.response?.data?.error || "Erreur lors de la création du compte";
|
|
||||||
|
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
toast({
|
toast({
|
||||||
@@ -102,11 +117,13 @@ export function AuthWrapper({ teams, initialUser }: AuthWrapperProps) {
|
|||||||
try {
|
try {
|
||||||
await authClient.logout();
|
await authClient.logout();
|
||||||
// Rediriger vers la page de login après déconnexion
|
// Rediriger vers la page de login après déconnexion
|
||||||
window.location.href = "/login";
|
router.push("/login");
|
||||||
|
router.refresh();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Logout failed:", error);
|
console.error("Logout failed:", error);
|
||||||
// En cas d'erreur, forcer le rechargement pour nettoyer l'état
|
// En cas d'erreur, forcer le rechargement pour nettoyer l'état
|
||||||
window.location.reload();
|
router.push("/login");
|
||||||
|
router.refresh();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
// Pages publiques (API auth et teams)
|
||||||
const { pathname } = request.nextUrl;
|
if (pathname.startsWith("/api/auth") || pathname.startsWith("/api/teams")) {
|
||||||
|
|
||||||
// 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))
|
|
||||||
) {
|
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier si c'est un fichier statique
|
// Si connecté et sur login, rediriger vers home
|
||||||
if (
|
if (isLoggedIn && isOnLoginPage) {
|
||||||
pathname.includes("/_next/") ||
|
return NextResponse.redirect(new URL("/", req.url));
|
||||||
pathname.includes("/favicon.ico") ||
|
|
||||||
pathname.includes("/public/")
|
|
||||||
) {
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier le cookie d'authentification (maintenant un UUID)
|
// Si non connecté et pas sur login, rediriger vers login
|
||||||
const userUuid = request.cookies.get(COOKIE_NAME)?.value;
|
if (!isLoggedIn && !isOnLoginPage) {
|
||||||
if (!userUuid) {
|
return NextResponse.redirect(new URL("/login", req.url));
|
||||||
// Rediriger vers la page de login si pas authentifié
|
|
||||||
const loginUrl = new URL("/login", request.url);
|
|
||||||
return NextResponse.redirect(loginUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
});
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: [
|
matcher: [
|
||||||
|
|||||||
@@ -61,6 +61,7 @@
|
|||||||
"input-otp": "1.4.1",
|
"input-otp": "1.4.1",
|
||||||
"lucide-react": "^0.454.0",
|
"lucide-react": "^0.454.0",
|
||||||
"next": "15.2.4",
|
"next": "15.2.4",
|
||||||
|
"next-auth": "5.0.0-beta.29",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"pg": "^8.12.0",
|
"pg": "^8.12.0",
|
||||||
"react": "^19",
|
"react": "^19",
|
||||||
|
|||||||
76
pnpm-lock.yaml
generated
76
pnpm-lock.yaml
generated
@@ -149,6 +149,9 @@ importers:
|
|||||||
next:
|
next:
|
||||||
specifier: 15.2.4
|
specifier: 15.2.4
|
||||||
version: 15.2.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
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:
|
next-themes:
|
||||||
specifier: ^0.4.6
|
specifier: ^0.4.6
|
||||||
version: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
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==}
|
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||||
engines: {node: '>=10'}
|
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':
|
'@babel/runtime@7.28.3':
|
||||||
resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==}
|
resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -607,6 +624,9 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@panva/hkdf@1.2.1':
|
||||||
|
resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==}
|
||||||
|
|
||||||
'@radix-ui/number@1.1.0':
|
'@radix-ui/number@1.1.0':
|
||||||
resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==}
|
resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==}
|
||||||
|
|
||||||
@@ -1612,6 +1632,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==}
|
resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
jose@6.1.0:
|
||||||
|
resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==}
|
||||||
|
|
||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
@@ -1712,6 +1735,22 @@ packages:
|
|||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
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:
|
next-themes@0.4.6:
|
||||||
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
|
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1754,6 +1793,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
|
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
oauth4webapi@3.8.2:
|
||||||
|
resolution: {integrity: sha512-FzZZ+bht5X0FKe7Mwz3DAVAmlH1BV5blSak/lHMBKz0/EBMhX6B10GlQYI51+oRp8ObJaX0g6pXrAxZh5s8rjw==}
|
||||||
|
|
||||||
object-assign@4.1.1:
|
object-assign@4.1.1:
|
||||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -1822,6 +1864,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
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:
|
prop-types@15.8.1:
|
||||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||||
|
|
||||||
@@ -2055,6 +2105,14 @@ snapshots:
|
|||||||
|
|
||||||
'@alloc/quick-lru@5.2.0': {}
|
'@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': {}
|
'@babel/runtime@7.28.3': {}
|
||||||
|
|
||||||
'@date-fns/tz@1.2.0': {}
|
'@date-fns/tz@1.2.0': {}
|
||||||
@@ -2307,6 +2365,8 @@ snapshots:
|
|||||||
'@next/swc-win32-x64-msvc@15.2.4':
|
'@next/swc-win32-x64-msvc@15.2.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@panva/hkdf@1.2.1': {}
|
||||||
|
|
||||||
'@radix-ui/number@1.1.0': {}
|
'@radix-ui/number@1.1.0': {}
|
||||||
|
|
||||||
'@radix-ui/primitive@1.1.1': {}
|
'@radix-ui/primitive@1.1.1': {}
|
||||||
@@ -3341,6 +3401,8 @@ snapshots:
|
|||||||
|
|
||||||
jiti@2.5.1: {}
|
jiti@2.5.1: {}
|
||||||
|
|
||||||
|
jose@6.1.0: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
lightningcss-darwin-arm64@1.30.1:
|
lightningcss-darwin-arm64@1.30.1:
|
||||||
@@ -3412,6 +3474,12 @@ snapshots:
|
|||||||
|
|
||||||
nanoid@3.3.11: {}
|
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):
|
next-themes@0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.1
|
react: 19.1.1
|
||||||
@@ -3450,6 +3518,8 @@ snapshots:
|
|||||||
|
|
||||||
normalize-range@0.1.2: {}
|
normalize-range@0.1.2: {}
|
||||||
|
|
||||||
|
oauth4webapi@3.8.2: {}
|
||||||
|
|
||||||
object-assign@4.1.1: {}
|
object-assign@4.1.1: {}
|
||||||
|
|
||||||
pg-cloudflare@1.2.7:
|
pg-cloudflare@1.2.7:
|
||||||
@@ -3513,6 +3583,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
xtend: 4.0.2
|
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:
|
prop-types@15.8.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
|
|||||||
@@ -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<string | null> {
|
|
||||||
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<boolean> {
|
|
||||||
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<NextResponse> {
|
|
||||||
// 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<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,6 +1,5 @@
|
|||||||
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,8 +120,12 @@ export class EvaluationService {
|
|||||||
try {
|
try {
|
||||||
await client.query("BEGIN");
|
await client.query("BEGIN");
|
||||||
|
|
||||||
// 1. Upsert user avec UUID
|
// 1. Récupérer le userUuid depuis le profile de l'évaluation
|
||||||
const userUuid = await AuthService.getUserUuidFromCookie();
|
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
|
// 2. Upsert user_evaluation avec user_uuid
|
||||||
const userEvalQuery = `
|
const userEvalQuery = `
|
||||||
@@ -624,7 +627,12 @@ export class EvaluationService {
|
|||||||
try {
|
try {
|
||||||
await client.query("BEGIN");
|
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
|
// Supprimer directement la skill evaluation
|
||||||
const deleteQuery = `
|
const deleteQuery = `
|
||||||
|
|||||||
@@ -22,6 +22,3 @@ export { AdminService } from "./admin-service";
|
|||||||
|
|
||||||
// Admin types (can be imported anywhere)
|
// Admin types (can be imported anywhere)
|
||||||
export type { TeamMember, TeamStats, DirectionStats } from "@/lib/admin-types";
|
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";
|
|
||||||
|
|||||||
28
types/next-auth.d.ts
vendored
Normal file
28
types/next-auth.d.ts
vendored
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user