From 508847940c8c35fd982ab935f4d69371869eed5a Mon Sep 17 00:00:00 2001 From: Luc SORIGNET Date: Sat, 22 Feb 2025 13:05:01 +0100 Subject: [PATCH] refactor: Creation d'un provider et d'un systeme de middleware --- Front-End/.env | 4 +- Front-End/docker/entrypoint.sh | 2 +- Front-End/next.config.mjs | 12 ++++-- Front-End/prod.env | 4 +- Front-End/src/app/[locale]/admin/layout.js | 30 ++++++++++--- Front-End/src/app/[locale]/parents/layout.js | 3 -- .../app/[locale]/parents/messagerie/page.js | 7 ++-- Front-End/src/app/actions/authAction.js | 24 +++++++++++ Front-End/src/app/layout.js | 17 ++++---- Front-End/src/components/Providers.js | 21 ++++++++++ Front-End/src/context/CsrfContext.js | 17 ++++++-- Front-End/src/hooks/useLocale.js | 12 ++++++ Front-End/src/instrumentation.js | 7 ++-- Front-End/src/middleware.js | 27 ++---------- Front-End/src/middlewares/stackHandler.js | 20 +++++++++ Front-End/src/middlewares/withLocale.js | 42 +++++++++++++++++++ Front-End/src/pages/api/auth/[...nextauth].js | 27 ++++++------ Front-End/src/utils/gravatar.js | 11 +++++ 18 files changed, 218 insertions(+), 69 deletions(-) create mode 100644 Front-End/src/components/Providers.js create mode 100644 Front-End/src/hooks/useLocale.js create mode 100644 Front-End/src/middlewares/stackHandler.js create mode 100644 Front-End/src/middlewares/withLocale.js create mode 100644 Front-End/src/utils/gravatar.js diff --git a/Front-End/.env b/Front-End/.env index f95783e..992c7ed 100644 --- a/Front-End/.env +++ b/Front-End/.env @@ -1,2 +1,4 @@ NEXT_PUBLIC_API_URL=http://localhost:8080 -NEXT_PUBLIC_USE_FAKE_DATA='false' \ No newline at end of file +NEXT_PUBLIC_USE_FAKE_DATA='false' +AUTH_SECRET='false' +NEXTAUTH_URL=http://localhost:3000 \ No newline at end of file diff --git a/Front-End/docker/entrypoint.sh b/Front-End/docker/entrypoint.sh index 3b8d773..5105beb 100644 --- a/Front-End/docker/entrypoint.sh +++ b/Front-End/docker/entrypoint.sh @@ -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) value=$(echo $line | cut -d "=" -f2) diff --git a/Front-End/next.config.mjs b/Front-End/next.config.mjs index b795174..2a12803 100644 --- a/Front-End/next.config.mjs +++ b/Front-End/next.config.mjs @@ -9,14 +9,20 @@ const nextConfig = { experimental: { instrumentationHook: true, }, - images: { - domains:['i.pravatar.cc'], + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "www.gravatar.com", + }, + ], }, env: { NEXT_PUBLIC_APP_VERSION: pkg.version, 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', - + AUTH_SECRET: process.env.AUTH_SECRET || 'false', + NEXTAUTH_URL: process.env.NEXTAUTH_URL || "http://localhost:3000", }, }; diff --git a/Front-End/prod.env b/Front-End/prod.env index cd1b68a..687be24 100644 --- a/Front-End/prod.env +++ b/Front-End/prod.env @@ -1,2 +1,4 @@ NEXT_PUBLIC_API_URL=_NEXT_PUBLIC_API_URL_ -NEXT_PUBLIC_USE_FAKE_DATA=_NEXT_PUBLIC_USE_FAKE_DATA_ \ No newline at end of file +NEXT_PUBLIC_USE_FAKE_DATA=_NEXT_PUBLIC_USE_FAKE_DATA_ +AUTH_SECRET=_AUTH_SECRET_ +NEXTAUTH_URL=_NEXTAUTH_URL_ \ No newline at end of file diff --git a/Front-End/src/app/[locale]/admin/layout.js b/Front-End/src/app/[locale]/admin/layout.js index caf3ad9..f664d9e 100644 --- a/Front-End/src/app/[locale]/admin/layout.js +++ b/Front-End/src/app/[locale]/admin/layout.js @@ -26,10 +26,11 @@ import { FE_ADMIN_SETTINGS_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 ProtectedRoute from '@/components/ProtectedRoute'; -import { SessionProvider } from 'next-auth/react'; +import { getGravatarUrl } from '@/utils/gravatar'; export default function Layout({ children, @@ -48,6 +49,8 @@ export default function Layout({ const [establishment, setEstablishment] = useState(null); const [isLoading, setIsLoading] = useState(false); const [isPopupVisible, setIsPopupVisible] = useState(false); + const [user, setUser] = useState(null); + const { data: session } = useSession(); const pathname = usePathname(); const currentPage = pathname.split('/').pop(); @@ -84,8 +87,18 @@ export default function Layout({ .finally(() => setIsLoading(false)); }, []); + useEffect(() => { + const fetchUser = async () => { + if (session) { // Vérifier que la session existe + const userData = await getUser(); + setUser(userData); + } + }; + + fetchUser(); + }, [session]); + return ( - {!isLoading && (
@@ -95,7 +108,15 @@ export default function Layout({
{headerTitle}
} + buttonContent={ + Profile + } items={dropdownItems} buttonClassName="" 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)} /> - ); } diff --git a/Front-End/src/app/[locale]/parents/layout.js b/Front-End/src/app/[locale]/parents/layout.js index cb84b03..b21b38c 100644 --- a/Front-End/src/app/[locale]/parents/layout.js +++ b/Front-End/src/app/[locale]/parents/layout.js @@ -9,7 +9,6 @@ import { FE_PARENTS_HOME_URL,FE_PARENTS_MESSAGERIE_URL,FE_PARENTS_SETTINGS_URL import useLocalStorage from '@/hooks/useLocalStorage'; import { fetchMessages } from '@/app/actions/messagerieAction'; import ProtectedRoute from '@/components/ProtectedRoute'; -import { SessionProvider } from 'next-auth/react'; import { disconnect } from '@/app/actions/authAction'; import Popup from '@/components/Popup'; @@ -55,7 +54,6 @@ export default function Layout({ } return ( -
{/* Entête */} @@ -111,7 +109,6 @@ export default function Layout({ onCancel={() => setIsPopupVisible(false)} /> - ); } diff --git a/Front-End/src/app/[locale]/parents/messagerie/page.js b/Front-End/src/app/[locale]/parents/messagerie/page.js index 9306564..1d40809 100644 --- a/Front-End/src/app/[locale]/parents/messagerie/page.js +++ b/Front-End/src/app/[locale]/parents/messagerie/page.js @@ -2,11 +2,12 @@ import React, { useState, useRef, useEffect } from 'react'; import { SendHorizontal } from 'lucide-react'; import Image from 'next/image'; +import { getGravatarUrl } from '@/utils/gravatar'; const contacts = [ - { id: 1, name: 'Facturation', profilePic: 'https://i.pravatar.cc/32' }, - { id: 2, name: 'Enseignant 1', profilePic: 'https://i.pravatar.cc/32' }, - { id: 3, name: 'Contact', profilePic: 'https://i.pravatar.cc/32' }, + { id: 1, name: 'Facturation', profilePic: getGravatarUrl('facturation@n3wtschool.com') }, + { id: 2, name: 'Enseignant 1', profilePic: getGravatarUrl('enseignant@n3wtschool.com') }, + { id: 3, name: 'Contact', profilePic: getGravatarUrl('contact@n3wtschool.com') }, ]; export default function MessageriePage() { diff --git a/Front-End/src/app/actions/authAction.js b/Front-End/src/app/actions/authAction.js index df79cbd..1c7cd91 100644 --- a/Front-End/src/app/actions/authAction.js +++ b/Front-End/src/app/actions/authAction.js @@ -160,4 +160,28 @@ export const getResetPassword = (uuid) => { 'Content-Type': 'application/json', }, }).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; + } }; \ No newline at end of file diff --git a/Front-End/src/app/layout.js b/Front-End/src/app/layout.js index 21e0d14..21ac084 100644 --- a/Front-End/src/app/layout.js +++ b/Front-End/src/app/layout.js @@ -1,8 +1,8 @@ import React from 'react'; import { getMessages } from 'next-intl/server'; -import { NextIntlClientProvider } from 'next-intl'; -import { CsrfProvider } from '@/context/CsrfContext'; +import Providers from '@/components/Providers' import "@/css/tailwind.css"; +import { headers } from 'next/headers'; export const metadata = { title: "N3WT-SCHOOL", @@ -22,17 +22,16 @@ export const metadata = { }; export default async function RootLayout({ children, params }) { - const { locale } = params; - const messages = await getMessages(locale); // Passez le locale ici + const headersList = headers(); + const locale = headersList.get('x-locale') || 'fr'; + const messages = await getMessages(locale); return ( - - {/* Passez le locale ici */} - {children} - - + + {children} + ); diff --git a/Front-End/src/components/Providers.js b/Front-End/src/components/Providers.js new file mode 100644 index 0000000..ef8a3de --- /dev/null +++ b/Front-End/src/components/Providers.js @@ -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 ( + + + + {children} + + + + ) +} \ No newline at end of file diff --git a/Front-End/src/context/CsrfContext.js b/Front-End/src/context/CsrfContext.js index cf96e94..2e9b697 100644 --- a/Front-End/src/context/CsrfContext.js +++ b/Front-End/src/context/CsrfContext.js @@ -8,8 +8,12 @@ const CsrfContext = createContext(); export const CsrfProvider = ({ children }) => { const [csrfToken, setCsrfTokenState] = useState(''); + const [isLoading, setIsLoading] = useState(true); useEffect(() => { + // Éviter les appels multiples si le token existe déjà + if (csrfToken) return; + fetch(`${BE_AUTH_CSRF_URL}`, { method: 'GET', credentials: 'include' // Inclut les cookies dans la requête @@ -24,16 +28,23 @@ export const CsrfProvider = ({ children }) => { }) .catch(error => { console.error('Error fetching CSRF token:', error); + }) + .finally(() => { + setIsLoading(false); }); - }, []); + }, []); // Dépendance vide pour n'exécuter qu'une seule fois return ( - {children} + {!isLoading && children} ); }; 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; }; \ No newline at end of file diff --git a/Front-End/src/hooks/useLocale.js b/Front-End/src/hooks/useLocale.js new file mode 100644 index 0000000..461026c --- /dev/null +++ b/Front-End/src/hooks/useLocale.js @@ -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'; + } +} diff --git a/Front-End/src/instrumentation.js b/Front-End/src/instrumentation.js index da8332b..fb6dc92 100644 --- a/Front-End/src/instrumentation.js +++ b/Front-End/src/instrumentation.js @@ -1,7 +1,6 @@ export async function register() { - - if (process.env.NEXT_RUNTIME === 'nodejs') { - await require('pino') - await require('next-logger') + if (process.env.NEXT_RUNTIME === 'nodejs' && process.env.NODE_ENV === 'production') { + await require('pino') + await require('next-logger') } } diff --git a/Front-End/src/middleware.js b/Front-End/src/middleware.js index 3819688..85c7660 100644 --- a/Front-End/src/middleware.js +++ b/Front-End/src/middleware.js @@ -1,24 +1,5 @@ -import { NextResponse } from 'next/server'; -import createMiddleware from 'next-intl/middleware'; -import { routing } from '@/i18n/routing'; -const middleware = createMiddleware(routing); +import { stackMiddlewares } from "@/middlewares/stackHandler"; +import { withLocale } from "@/middlewares/withLocale"; -export default function handler(req) { - const { pathname, search } = req.nextUrl; - 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).*)'], -}; \ No newline at end of file +const middlewares = [withLocale]; +export default stackMiddlewares(middlewares); \ No newline at end of file diff --git a/Front-End/src/middlewares/stackHandler.js b/Front-End/src/middlewares/stackHandler.js new file mode 100644 index 0000000..a9dfb4d --- /dev/null +++ b/Front-End/src/middlewares/stackHandler.js @@ -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); + }; +} \ No newline at end of file diff --git a/Front-End/src/middlewares/withLocale.js b/Front-End/src/middlewares/withLocale.js new file mode 100644 index 0000000..5fc5d52 --- /dev/null +++ b/Front-End/src/middlewares/withLocale.js @@ -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 }; \ No newline at end of file diff --git a/Front-End/src/pages/api/auth/[...nextauth].js b/Front-End/src/pages/api/auth/[...nextauth].js index df190df..35ad508 100644 --- a/Front-End/src/pages/api/auth/[...nextauth].js +++ b/Front-End/src/pages/api/auth/[...nextauth].js @@ -1,10 +1,10 @@ import NextAuth from 'next-auth'; import CredentialsProvider from 'next-auth/providers/credentials'; import { getJWT, refreshJWT } from '@/app/actions/authAction'; - -import jwt_decode from 'jsonwebtoken'; // Changed import +import jwt_decode from 'jsonwebtoken'; const options = { + secret: process.env.AUTH_SECRET, providers: [ CredentialsProvider({ name: 'Credentials', @@ -50,9 +50,9 @@ const options = { } }, callbacks: { - async jwt({ token, user, trigger }) { + async jwt({ token, user }) { // Si c'est la première connexion - if (user) { + if (user && user?.token) { return { ...token, token: user.token, @@ -69,23 +69,24 @@ const options = { // Token expiré, essayer de le rafraîchir try { const response = await refreshJWT({ refresh: token.refresh }); - if (!response) { + if (response && response?.token) { + return { + ...token, + token: response.token, + refresh: response.refresh, + tokenExpires: jwt_decode.decode(response.token).exp * 1000 + }; + } + else{ throw new Error('Failed to refresh token'); } - - return { - ...token, - token: response.token, - refresh: response.refresh, - tokenExpires: jwt_decode.decode(response.token).exp * 1000 - }; } catch (error) { console.error("Refresh token failed:", error); return token; } }, async session({ session, token }) { - if (token) { + if (token && token?.token) { const {user_id, droit, email} = jwt_decode.decode(token.token); session.user = { ...session.user, diff --git a/Front-End/src/utils/gravatar.js b/Front-End/src/utils/gravatar.js new file mode 100644 index 0000000..7a64408 --- /dev/null +++ b/Front-End/src/utils/gravatar.js @@ -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`; +} +}