refactor: Creation d'un provider et d'un systeme de middleware

This commit is contained in:
Luc SORIGNET
2025-02-22 13:05:01 +01:00
parent c861239d48
commit 508847940c
18 changed files with 218 additions and 69 deletions

View File

@ -1,2 +1,4 @@
NEXT_PUBLIC_API_URL=http://localhost:8080 NEXT_PUBLIC_API_URL=http://localhost:8080
NEXT_PUBLIC_USE_FAKE_DATA='false' NEXT_PUBLIC_USE_FAKE_DATA='false'
AUTH_SECRET='false'
NEXTAUTH_URL=http://localhost:3000

View File

@ -15,7 +15,7 @@ replace_value() {
} }
printenv | grep NEXT_PUBLIC_ | while read -r line ; do printenv | while read -r line ; do
key=$(echo $line | cut -d "=" -f1) key=$(echo $line | cut -d "=" -f1)
value=$(echo $line | cut -d "=" -f2) value=$(echo $line | cut -d "=" -f2)

View File

@ -10,13 +10,19 @@ const nextConfig = {
instrumentationHook: true, instrumentationHook: true,
}, },
images: { images: {
domains:['i.pravatar.cc'], remotePatterns: [
{
protocol: "https",
hostname: "www.gravatar.com",
},
],
}, },
env: { env: {
NEXT_PUBLIC_APP_VERSION: pkg.version, NEXT_PUBLIC_APP_VERSION: pkg.version,
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080", NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080",
NEXT_PUBLIC_USE_FAKE_DATA: process.env.NEXT_PUBLIC_USE_FAKE_DATA || 'false', NEXT_PUBLIC_USE_FAKE_DATA: process.env.NEXT_PUBLIC_USE_FAKE_DATA || 'false',
AUTH_SECRET: process.env.AUTH_SECRET || 'false',
NEXTAUTH_URL: process.env.NEXTAUTH_URL || "http://localhost:3000",
}, },
}; };

View File

@ -1,2 +1,4 @@
NEXT_PUBLIC_API_URL=_NEXT_PUBLIC_API_URL_ NEXT_PUBLIC_API_URL=_NEXT_PUBLIC_API_URL_
NEXT_PUBLIC_USE_FAKE_DATA=_NEXT_PUBLIC_USE_FAKE_DATA_ NEXT_PUBLIC_USE_FAKE_DATA=_NEXT_PUBLIC_USE_FAKE_DATA_
AUTH_SECRET=_AUTH_SECRET_
NEXTAUTH_URL=_NEXTAUTH_URL_

View File

@ -26,10 +26,11 @@ import {
FE_ADMIN_SETTINGS_URL FE_ADMIN_SETTINGS_URL
} from '@/utils/Url'; } from '@/utils/Url';
import { disconnect } from '@/app/actions/authAction'; import { disconnect, getUser } from '@/app/actions/authAction';
import { useSession } from 'next-auth/react';
import { fetchEstablishment } from '@/app/actions/schoolAction'; import { fetchEstablishment } from '@/app/actions/schoolAction';
import ProtectedRoute from '@/components/ProtectedRoute'; import ProtectedRoute from '@/components/ProtectedRoute';
import { SessionProvider } from 'next-auth/react'; import { getGravatarUrl } from '@/utils/gravatar';
export default function Layout({ export default function Layout({
children, children,
@ -48,6 +49,8 @@ export default function Layout({
const [establishment, setEstablishment] = useState(null); const [establishment, setEstablishment] = useState(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isPopupVisible, setIsPopupVisible] = useState(false); const [isPopupVisible, setIsPopupVisible] = useState(false);
const [user, setUser] = useState(null);
const { data: session } = useSession();
const pathname = usePathname(); const pathname = usePathname();
const currentPage = pathname.split('/').pop(); const currentPage = pathname.split('/').pop();
@ -84,8 +87,18 @@ export default function Layout({
.finally(() => setIsLoading(false)); .finally(() => setIsLoading(false));
}, []); }, []);
useEffect(() => {
const fetchUser = async () => {
if (session) { // Vérifier que la session existe
const userData = await getUser();
setUser(userData);
}
};
fetchUser();
}, [session]);
return ( return (
<SessionProvider>
<ProtectedRoute> <ProtectedRoute>
{!isLoading && ( {!isLoading && (
<div className="flex min-h-screen bg-gray-50"> <div className="flex min-h-screen bg-gray-50">
@ -95,7 +108,15 @@ export default function Layout({
<header className="h-16 bg-white border-b border-gray-200 px-8 py-4 flex items-center justify-between z-9"> <header className="h-16 bg-white border-b border-gray-200 px-8 py-4 flex items-center justify-between z-9">
<div className="text-xl font-semibold">{headerTitle}</div> <div className="text-xl font-semibold">{headerTitle}</div>
<DropdownMenu <DropdownMenu
buttonContent={<Image src="https://i.pravatar.cc/32" alt="Profile" className="w-8 h-8 rounded-full cursor-pointer" width={150} height={150} />} buttonContent={
<Image
src={getGravatarUrl(user?.email)}
alt="Profile"
className="w-8 h-8 rounded-full cursor-pointer"
width={32}
height={32}
/>
}
items={dropdownItems} items={dropdownItems}
buttonClassName="" buttonClassName=""
menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded shadow-lg" menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded shadow-lg"
@ -126,7 +147,6 @@ export default function Layout({
onCancel={() => setIsPopupVisible(false)} onCancel={() => setIsPopupVisible(false)}
/> />
</ProtectedRoute> </ProtectedRoute>
</SessionProvider>
); );
} }

View File

@ -9,7 +9,6 @@ import { FE_PARENTS_HOME_URL,FE_PARENTS_MESSAGERIE_URL,FE_PARENTS_SETTINGS_URL
import useLocalStorage from '@/hooks/useLocalStorage'; import useLocalStorage from '@/hooks/useLocalStorage';
import { fetchMessages } from '@/app/actions/messagerieAction'; import { fetchMessages } from '@/app/actions/messagerieAction';
import ProtectedRoute from '@/components/ProtectedRoute'; import ProtectedRoute from '@/components/ProtectedRoute';
import { SessionProvider } from 'next-auth/react';
import { disconnect } from '@/app/actions/authAction'; import { disconnect } from '@/app/actions/authAction';
import Popup from '@/components/Popup'; import Popup from '@/components/Popup';
@ -55,7 +54,6 @@ export default function Layout({
} }
return ( return (
<SessionProvider>
<ProtectedRoute> <ProtectedRoute>
<div className="flex flex-col min-h-screen bg-gray-50"> <div className="flex flex-col min-h-screen bg-gray-50">
{/* Entête */} {/* Entête */}
@ -111,7 +109,6 @@ export default function Layout({
onCancel={() => setIsPopupVisible(false)} onCancel={() => setIsPopupVisible(false)}
/> />
</ProtectedRoute> </ProtectedRoute>
</SessionProvider>
); );
} }

View File

@ -2,11 +2,12 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { SendHorizontal } from 'lucide-react'; import { SendHorizontal } from 'lucide-react';
import Image from 'next/image'; import Image from 'next/image';
import { getGravatarUrl } from '@/utils/gravatar';
const contacts = [ const contacts = [
{ id: 1, name: 'Facturation', profilePic: 'https://i.pravatar.cc/32' }, { id: 1, name: 'Facturation', profilePic: getGravatarUrl('facturation@n3wtschool.com') },
{ id: 2, name: 'Enseignant 1', profilePic: 'https://i.pravatar.cc/32' }, { id: 2, name: 'Enseignant 1', profilePic: getGravatarUrl('enseignant@n3wtschool.com') },
{ id: 3, name: 'Contact', profilePic: 'https://i.pravatar.cc/32' }, { id: 3, name: 'Contact', profilePic: getGravatarUrl('contact@n3wtschool.com') },
]; ];
export default function MessageriePage() { export default function MessageriePage() {

View File

@ -161,3 +161,27 @@ export const getResetPassword = (uuid) => {
}, },
}).then(requestResponseHandler); }).then(requestResponseHandler);
}; };
/**
* Récupère les informations de l'utilisateur connecté depuis la session
* @returns {Promise} Les données de l'utilisateur
*/
export const getUser = async () => {
try {
const session = await getSession();
if (!session || !session.user) {
return null;
}
return {
id: session.user.user_id,
email: session.user.email,
role: session.user.droit
};
} catch (error) {
console.error('Error getting user from session:', error);
throw error;
}
};

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { getMessages } from 'next-intl/server'; import { getMessages } from 'next-intl/server';
import { NextIntlClientProvider } from 'next-intl'; import Providers from '@/components/Providers'
import { CsrfProvider } from '@/context/CsrfContext';
import "@/css/tailwind.css"; import "@/css/tailwind.css";
import { headers } from 'next/headers';
export const metadata = { export const metadata = {
title: "N3WT-SCHOOL", title: "N3WT-SCHOOL",
@ -22,17 +22,16 @@ export const metadata = {
}; };
export default async function RootLayout({ children, params }) { export default async function RootLayout({ children, params }) {
const { locale } = params; const headersList = headers();
const messages = await getMessages(locale); // Passez le locale ici const locale = headersList.get('x-locale') || 'fr';
const messages = await getMessages(locale);
return ( return (
<html lang={locale}> <html lang={locale}>
<body> <body>
<CsrfProvider> <Providers messages={messages} locale={locale} session={params.session}>
<NextIntlClientProvider messages={messages} locale={locale}> {/* Passez le locale ici */}
{children} {children}
</NextIntlClientProvider> </Providers>
</CsrfProvider>
</body> </body>
</html> </html>
); );

View File

@ -0,0 +1,21 @@
'use client'
import { SessionProvider } from "next-auth/react"
import { CsrfProvider } from '@/context/CsrfContext'
import { NextIntlClientProvider } from 'next-intl'
export default function Providers({ children, messages, locale, session }) {
if (!locale) {
console.error('Locale non définie dans Providers');
locale = 'fr'; // Valeur par défaut
}
return (
<SessionProvider session={session}>
<CsrfProvider>
<NextIntlClientProvider messages={messages} locale={locale}>
{children}
</NextIntlClientProvider>
</CsrfProvider>
</SessionProvider>
)
}

View File

@ -8,8 +8,12 @@ const CsrfContext = createContext();
export const CsrfProvider = ({ children }) => { export const CsrfProvider = ({ children }) => {
const [csrfToken, setCsrfTokenState] = useState(''); const [csrfToken, setCsrfTokenState] = useState('');
const [isLoading, setIsLoading] = useState(true);
useEffect(() => { useEffect(() => {
// Éviter les appels multiples si le token existe déjà
if (csrfToken) return;
fetch(`${BE_AUTH_CSRF_URL}`, { fetch(`${BE_AUTH_CSRF_URL}`, {
method: 'GET', method: 'GET',
credentials: 'include' // Inclut les cookies dans la requête credentials: 'include' // Inclut les cookies dans la requête
@ -24,16 +28,23 @@ export const CsrfProvider = ({ children }) => {
}) })
.catch(error => { .catch(error => {
console.error('Error fetching CSRF token:', error); console.error('Error fetching CSRF token:', error);
})
.finally(() => {
setIsLoading(false);
}); });
}, []); }, []); // Dépendance vide pour n'exécuter qu'une seule fois
return ( return (
<CsrfContext.Provider value={csrfToken}> <CsrfContext.Provider value={csrfToken}>
{children} {!isLoading && children}
</CsrfContext.Provider> </CsrfContext.Provider>
); );
}; };
export const useCsrfToken = () => { export const useCsrfToken = () => {
return useContext(CsrfContext); const context = useContext(CsrfContext);
if (context === undefined) {
throw new Error('useCsrfToken must be used within a CsrfProvider');
}
return context;
}; };

View File

@ -0,0 +1,12 @@
'use client';
import { headers } from 'next/headers';
export function useLocale() {
try {
const headersList = headers();
return headersList.get('x-locale') || 'fr';
} catch {
return 'fr';
}
}

View File

@ -1,6 +1,5 @@
export async function register() { export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs' && process.env.NODE_ENV === 'production') {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await require('pino') await require('pino')
await require('next-logger') await require('next-logger')
} }

View File

@ -1,24 +1,5 @@
import { NextResponse } from 'next/server'; import { stackMiddlewares } from "@/middlewares/stackHandler";
import createMiddleware from 'next-intl/middleware'; import { withLocale } from "@/middlewares/withLocale";
import { routing } from '@/i18n/routing';
const middleware = createMiddleware(routing);
export default function handler(req) { const middlewares = [withLocale];
const { pathname, search } = req.nextUrl; export default stackMiddlewares(middlewares);
const locale = pathname.split('/')[1]; // Obtenez la locale actuelle
// Vérifiez si la route ne contient pas de locale
if (!pathname.startsWith('/fr') && !pathname.startsWith('/en')) {
// Redirigez vers la locale par défaut (fr) avec les paramètres de recherche
console.log('Redirecting to /fr');
return NextResponse.redirect(new URL(`/fr${pathname}${search}`, req.url));
}
return middleware(req);
}
export const config = {
// Match only internationalized pathnames
matcher: ['/', '/(fr|en)/:path*','/((?!api|_next|favicon.ico|favicon.svg).*)'],
};

View File

@ -0,0 +1,20 @@
import { NextResponse } from "next/server";
export function stackMiddlewares(functions = []) {
return async (request, event) => {
let index = 0;
const next = async (req, evt) => {
const current = functions[index];
index++;
if (current) {
return current(next)(req, evt);
}
return NextResponse.next();
};
return next(request, event);
};
}

View File

@ -0,0 +1,42 @@
import { NextResponse } from 'next/server';
import createIntlMiddleware from 'next-intl/middleware';
import { routing } from '@/i18n/routing';
const withLocale = (next) => {
const intlMiddleware = createIntlMiddleware(routing);
return async (request, event) => {
const { pathname, search } = request.nextUrl;
// Ignorer les ressources statiques et API routes
if (pathname.startsWith('/_next') || pathname.startsWith('/api') || pathname.includes('.')) {
return next(request, event);
}
// Déterminer la locale
let locale = 'fr';
if (pathname.startsWith('/fr') || pathname.startsWith('/en')) {
locale = pathname.split('/')[1];
}
// Si pas de locale dans l'URL, rediriger vers /fr
if (!pathname.startsWith('/fr') && !pathname.startsWith('/en')) {
return NextResponse.redirect(new URL(`/fr${pathname}${search}`, request.url));
}
// Appliquer le middleware next-intl
const response = await intlMiddleware(request, event);
if (response) {
// Ajouter la locale aux headers
response.headers.set('x-locale', locale);
return response;
}
const nextResponse = await next(request, event);
// Ajouter la locale aux headers même pour les réponses suivantes
nextResponse.headers.set('x-locale', locale);
return nextResponse;
};
};
export { withLocale };

View File

@ -1,10 +1,10 @@
import NextAuth from 'next-auth'; import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials'; import CredentialsProvider from 'next-auth/providers/credentials';
import { getJWT, refreshJWT } from '@/app/actions/authAction'; import { getJWT, refreshJWT } from '@/app/actions/authAction';
import jwt_decode from 'jsonwebtoken';
import jwt_decode from 'jsonwebtoken'; // Changed import
const options = { const options = {
secret: process.env.AUTH_SECRET,
providers: [ providers: [
CredentialsProvider({ CredentialsProvider({
name: 'Credentials', name: 'Credentials',
@ -50,9 +50,9 @@ const options = {
} }
}, },
callbacks: { callbacks: {
async jwt({ token, user, trigger }) { async jwt({ token, user }) {
// Si c'est la première connexion // Si c'est la première connexion
if (user) { if (user && user?.token) {
return { return {
...token, ...token,
token: user.token, token: user.token,
@ -69,23 +69,24 @@ const options = {
// Token expiré, essayer de le rafraîchir // Token expiré, essayer de le rafraîchir
try { try {
const response = await refreshJWT({ refresh: token.refresh }); const response = await refreshJWT({ refresh: token.refresh });
if (!response) { if (response && response?.token) {
throw new Error('Failed to refresh token');
}
return { return {
...token, ...token,
token: response.token, token: response.token,
refresh: response.refresh, refresh: response.refresh,
tokenExpires: jwt_decode.decode(response.token).exp * 1000 tokenExpires: jwt_decode.decode(response.token).exp * 1000
}; };
}
else{
throw new Error('Failed to refresh token');
}
} catch (error) { } catch (error) {
console.error("Refresh token failed:", error); console.error("Refresh token failed:", error);
return token; return token;
} }
}, },
async session({ session, token }) { async session({ session, token }) {
if (token) { if (token && token?.token) {
const {user_id, droit, email} = jwt_decode.decode(token.token); const {user_id, droit, email} = jwt_decode.decode(token.token);
session.user = { session.user = {
...session.user, ...session.user,

View File

@ -0,0 +1,11 @@
import crypto from 'crypto';
export function getGravatarUrl(email, size = 32) {
if(email === undefined || typeof email !== 'string') {
return 'https://www.gravatar.com/avatar/';
}
else {
const hash = crypto.createHash('md5').update(email.toLowerCase().trim()).digest('hex');
return `https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon`;
}
}