feat: Suite de la gestion des sessions

This commit is contained in:
N3WT DE COMPET
2025-02-17 16:11:15 +01:00
parent 65d5b8c424
commit 8ea68bbad0
18 changed files with 113 additions and 180 deletions

View File

@ -148,8 +148,7 @@ class ProfileSimpleView(APIView):
def delete(self, request, id): def delete(self, request, id):
return bdd.delete_object(Profile, id) return bdd.delete_object(Profile, id)
@method_decorator(csrf_protect, name='dispatch') @method_decorator(csrf_exempt, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class LoginView(APIView): class LoginView(APIView):
@swagger_auto_schema( @swagger_auto_schema(
operation_description="Connexion utilisateur", operation_description="Connexion utilisateur",
@ -168,13 +167,14 @@ class LoginView(APIView):
'errorFields': openapi.Schema(type=openapi.TYPE_OBJECT), 'errorFields': openapi.Schema(type=openapi.TYPE_OBJECT),
'errorMessage': openapi.Schema(type=openapi.TYPE_STRING), 'errorMessage': openapi.Schema(type=openapi.TYPE_STRING),
'profil': openapi.Schema(type=openapi.TYPE_INTEGER), 'profil': openapi.Schema(type=openapi.TYPE_INTEGER),
'droit': openapi.Schema(type=openapi.TYPE_INTEGER) 'droit': openapi.Schema(type=openapi.TYPE_INTEGER),
'id': openapi.Schema(type=openapi.TYPE_INTEGER),
} }
)) ))
} }
) )
def post(self, request): def post(self, request):
data=JSONParser().parse(request) data = JSONParser().parse(request)
validatorAuthentication = validator.ValidatorAuthentication(data=data) validatorAuthentication = validator.ValidatorAuthentication(data=data)
retour = error.returnMessage[error.WRONG_ID] retour = error.returnMessage[error.WRONG_ID]
validationOk, errorFields = validatorAuthentication.validate() validationOk, errorFields = validatorAuthentication.validate()
@ -196,13 +196,12 @@ class LoginView(APIView):
else: else:
retour = error.returnMessage[error.WRONG_ID] retour = error.returnMessage[error.WRONG_ID]
return JsonResponse({ return JsonResponse({
'errorFields':errorFields, 'errorFields': errorFields,
'errorMessage':retour, 'errorMessage': retour,
'profil':user.id if user else -1, 'profil': user.id if user else -1,
'droit':user.droit if user else -1, 'droit': user.droit if user else -1,
#'jwtToken':jwt_token if profil != -1 else '' 'id': user.id if user else -1,
}, safe=False) }, safe=False)
@method_decorator(csrf_protect, name='dispatch') @method_decorator(csrf_protect, name='dispatch')

View File

@ -252,19 +252,10 @@ CORS_ALLOW_ALL_HEADERS = True
CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_CREDENTIALS = True
CORS_ALLOWED_ORIGINS = [ CORS_ALLOWED_ORIGINS = [
'http://localhost:3000' os.getenv('CORS_ALLOWED_ORIGINS', 'http://localhost:3000')
] ]
CSRF_TRUSTED_ORIGINS = [ CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost:3000,http://localhost:8080').split(',')
'http://localhost:3000',
'http://localhost:8080'
]
# CORS_ALLOWED_ORIGINS = [
# os.getenv('CORS_ALLOWED_ORIGINS', 'http://localhost:3000')
# ]
# CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost:3000,http://localhost:8080').split(',')
CSRF_COOKIE_HTTPONLY = False CSRF_COOKIE_HTTPONLY = False
CSRF_COOKIE_SECURE = False CSRF_COOKIE_SECURE = False

View File

@ -26,6 +26,8 @@ import {
import { disconnect } from '@/app/lib/authAction'; import { disconnect } from '@/app/lib/authAction';
import { fetchEstablishment } from '@/app/lib/schoolAction'; import { fetchEstablishment } from '@/app/lib/schoolAction';
import ProtectedRoute from '@/components/ProtectedRoute';
import { SessionProvider } from 'next-auth/react';
export default function Layout({ export default function Layout({
children, children,
@ -73,6 +75,9 @@ export default function Layout({
return ( return (
<> <>
<SessionProvider>
<ProtectedRoute>
{!isLoading && ( {!isLoading && (
<div className="flex min-h-screen bg-gray-50"> <div className="flex min-h-screen bg-gray-50">
<Sidebar establishment={establishment} currentPage={currentPage} items={Object.values(sidebarItems)} className="h-full" /> <Sidebar establishment={establishment} currentPage={currentPage} items={Object.values(sidebarItems)} className="h-full" />
@ -105,6 +110,9 @@ export default function Layout({
</div> </div>
</div> </div>
)} )}
</ProtectedRoute>
</SessionProvider>
</> </>
); );
} }

View File

@ -4,7 +4,7 @@ import StructureManagement from '@/components/Structure/Configuration/StructureM
import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement'; import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement';
import FeesManagement from '@/components/Structure/Tarification/FeesManagement'; import FeesManagement from '@/components/Structure/Tarification/FeesManagement';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import useCsrfToken from '@/hooks/useCsrfToken'; import { useCsrfToken } from '@/context/CsrfContext';
import { ClassesProvider } from '@/context/ClassesContext'; import { ClassesProvider } from '@/context/ClassesContext';
import { createDatas, import { createDatas,
updateDatas, updateDatas,

View File

@ -3,7 +3,7 @@ import React, { useState, useEffect } from 'react';
import { useSearchParams, useRouter } from 'next/navigation'; import { useSearchParams, useRouter } from 'next/navigation';
import InscriptionFormShared from '@/components/Inscription/InscriptionFormShared'; import InscriptionFormShared from '@/components/Inscription/InscriptionFormShared';
import { FE_ADMIN_SUBSCRIPTIONS_URL } from '@/utils/Url'; import { FE_ADMIN_SUBSCRIPTIONS_URL } from '@/utils/Url';
import useCsrfToken from '@/hooks/useCsrfToken'; import { useCsrfToken } from '@/context/CsrfContext';
import { mockStudent } from '@/data/mockStudent'; import { mockStudent } from '@/data/mockStudent';
import { editRegisterForm, fetchRegisterForm } from '@/app/lib/subscriptionAction'; import { editRegisterForm, fetchRegisterForm } from '@/app/lib/subscriptionAction';

View File

@ -42,7 +42,7 @@ import {
FE_ADMIN_SUBSCRIPTIONS_EDIT_URL } from '@/utils/Url'; FE_ADMIN_SUBSCRIPTIONS_EDIT_URL } from '@/utils/Url';
import DjangoCSRFToken from '@/components/DjangoCSRFToken' import DjangoCSRFToken from '@/components/DjangoCSRFToken'
import useCsrfToken from '@/hooks/useCsrfToken'; import { useCsrfToken } from '@/context/CsrfContext';
import { fetchRegistrationFileGroups } from '@/app/lib/registerFileGroupAction'; import { fetchRegistrationFileGroups } from '@/app/lib/registerFileGroupAction';
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';

View File

@ -2,7 +2,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import InscriptionFormShared from '@/components/Inscription/InscriptionFormShared'; import InscriptionFormShared from '@/components/Inscription/InscriptionFormShared';
import { useSearchParams, useRouter } from 'next/navigation'; import { useSearchParams, useRouter } from 'next/navigation';
import useCsrfToken from '@/hooks/useCsrfToken'; import { useCsrfToken } from '@/context/CsrfContext';
import { FE_PARENTS_HOME_URL} from '@/utils/Url'; import { FE_PARENTS_HOME_URL} from '@/utils/Url';
import { editRegisterForm} from '@/app/lib/subscriptionAction'; import { editRegisterForm} from '@/app/lib/subscriptionAction';

View File

@ -9,9 +9,12 @@ import Button from '@/components/Button'; // Importez le composant Button
import { User, KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires import { User, KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires
import { import {
FE_USERS_NEW_PASSWORD_URL, FE_USERS_NEW_PASSWORD_URL,
BE_AUTH_INFO_SESSION } from '@/utils/Url'; BE_AUTH_INFO_SESSION,
FE_ADMIN_SUBSCRIPTIONS_URL,
FE_PARENTS_HOME_URL
} from '@/utils/Url';
import useLocalStorage from '@/hooks/useLocalStorage'; import useLocalStorage from '@/hooks/useLocalStorage';
import { signIn } from 'next-auth/react'; import { signIn, getSession } from 'next-auth/react';
import { useCsrfToken } from '@/context/CsrfContext'; // Importez le hook useCsrfToken import { useCsrfToken } from '@/context/CsrfContext'; // Importez le hook useCsrfToken
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
@ -33,32 +36,46 @@ export default function Page() {
return data.errorMessage === "" return data.errorMessage === ""
} }
function handleFormLogin(formData) { async function handleFormLogin(formData) {
setIsLoading(true); setIsLoading(true);
console.log('Form Data', Object.fromEntries(formData.entries())); // Affichez les entrées du FormData
console.log('csrf passé ', csrfToken); // Affichez le token CSRF
signIn('credentials', { try {
const result = await signIn('credentials', {
redirect: false, redirect: false,
email: formData.get('login'), email: formData.get('login'),
password: formData.get('password'), password: formData.get('password'),
csrfToken: csrfToken // Utilisez le token CSRF récupéré par le hook });
})
.then(result => {
console.log('Sign In Result', result); console.log('Sign In Result', result);
setIsLoading(false); setIsLoading(false);
if (result.error) { if (result.error) {
setErrorMessage(result.error); setErrorMessage(result.error);
} else { } else {
router.push(result.url); const session = await getSession();
if (!session || !session.user) {
throw new Error('Session not found');
} }
}) const user = session.user;
.catch(error => { console.log('User Session:', user);
localStorage.setItem('userId', user.id); // Stocker l'identifiant de l'utilisateur
if (user.droit === 0) {
// Vue ECOLE
} else if (user.droit === 1) {
// Vue ADMIN
router.push(FE_ADMIN_SUBSCRIPTIONS_URL);
} else if (user.droit === 2) {
// Vue PARENT
router.push(FE_PARENTS_HOME_URL);
} else {
// Cas anormal
}
}
} catch (error) {
console.error('Error during sign in:', error); console.error('Error during sign in:', error);
setIsLoading(false); setIsLoading(false);
setErrorMessage('An error occurred during sign in.'); setErrorMessage('An error occurred during sign in.');
}); }
} }
if (isLoading === true) { if (isLoading === true) {

View File

@ -10,7 +10,7 @@ import Button from '@/components/Button'; // Importez le composant Button
import Popup from '@/components/Popup'; // Importez le composant Popup import Popup from '@/components/Popup'; // Importez le composant Popup
import { User } from 'lucide-react'; // Importez directement les icônes nécessaires import { User } from 'lucide-react'; // Importez directement les icônes nécessaires
import { FE_USERS_LOGIN_URL } from '@/utils/Url'; import { FE_USERS_LOGIN_URL } from '@/utils/Url';
import useCsrfToken from '@/hooks/useCsrfToken'; import { useCsrfToken } from '@/context/CsrfContext';
import { sendNewPassword } from '@/app/lib/authAction'; import { sendNewPassword } from '@/app/lib/authAction';
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';

View File

@ -11,7 +11,7 @@ import Button from '@/components/Button'; // Importez le composant Button
import Popup from '@/components/Popup'; import Popup from '@/components/Popup';
import { FE_USERS_LOGIN_URL } from '@/utils/Url'; import { FE_USERS_LOGIN_URL } from '@/utils/Url';
import { KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires import { KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires
import useCsrfToken from '@/hooks/useCsrfToken'; import { useCsrfToken } from '@/context/CsrfContext';
import { getResetPassword, resetPassword } from '@/app/lib/authAction'; import { getResetPassword, resetPassword } from '@/app/lib/authAction';
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';

View File

@ -11,7 +11,7 @@ import Button from '@/components/Button'; // Importez le composant Button
import Popup from '@/components/Popup'; // Importez le composant Popup import Popup from '@/components/Popup'; // Importez le composant Popup
import { User, KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires import { User, KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires
import { FE_USERS_LOGIN_URL } from '@/utils/Url'; import { FE_USERS_LOGIN_URL } from '@/utils/Url';
import useCsrfToken from '@/hooks/useCsrfToken'; import { useCsrfToken } from '@/context/CsrfContext';
import { subscribe } from '@/app/lib/authAction'; import { subscribe } from '@/app/lib/authAction';
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';

View File

@ -1,8 +1,7 @@
import React from 'react'; import React from 'react';
import { NextIntlClientProvider } from 'next-intl';
import { CsrfProvider } from '@/context/CsrfContext'; // Importez le CsrfProvider
import { getMessages } from 'next-intl/server'; import { getMessages } from 'next-intl/server';
import { NextIntlClientProvider } from 'next-intl';
import { CsrfProvider } from '@/context/CsrfContext';
import "@/css/tailwind.css"; import "@/css/tailwind.css";
export const metadata = { export const metadata = {
@ -22,14 +21,15 @@ export const metadata = {
}, },
}; };
export default async function RootLayout({ children, params: { locale } }) { export default async function RootLayout({ children, params }) {
const messages = await getMessages(); const { locale } = params;
const messages = await getMessages(locale); // Passez le locale ici
return ( return (
<html lang={locale}> <html lang={locale}>
<body> <body>
<CsrfProvider> {/* Enveloppez votre application avec le CsrfProvider */} <CsrfProvider>
<NextIntlClientProvider messages={messages}> <NextIntlClientProvider messages={messages} locale={locale}> {/* Passez le locale ici */}
{children} {children}
</NextIntlClientProvider> </NextIntlClientProvider>
</CsrfProvider> </CsrfProvider>

View File

@ -1,24 +1,27 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import useLocalStorage from '@/hooks/useLocalStorage'; import { useSession } from 'next-auth/react';
import Loader from '@/components/Loader'; // Importez le composant Loader
import { FE_USERS_LOGIN_URL } from '@/utils/Url'; import { FE_USERS_LOGIN_URL } from '@/utils/Url';
const ProtectedRoute = ({ children }) => { const ProtectedRoute = ({ children }) => {
const { data: session, status } = useSession();
const router = useRouter(); const router = useRouter();
const [userId] = useLocalStorage("userId", '');
useEffect(() => { useEffect(() => {
if (!userId) { if (status === 'loading') return; // Ne rien faire tant que le statut est "loading"
if (!session) {
// Rediriger vers la page de login si l'utilisateur n'est pas connecté // Rediriger vers la page de login si l'utilisateur n'est pas connecté
router.push(FE_USERS_LOGIN_URL); router.push(`${FE_USERS_LOGIN_URL}`);
} }
}, [userId, router]); }, [session, status, router]);
if (!userId) { if (status === 'loading' || !session) {
return <div>Loading...</div>; return <Loader />; // Affichez un loader pendant le chargement ou si l'utilisateur n'est pas connecté
} }
// Afficher les enfants seulement si l'utilisateur est connecté // Afficher les enfants seulement si l'utilisateur est connecté
return userId ? children : null; return children;
}; };
export default ProtectedRoute; export default ProtectedRoute;

View File

@ -4,7 +4,7 @@ import Table from '@/components/Table';
import Popup from '@/components/Popup'; import Popup from '@/components/Popup';
import ToggleSwitch from '@/components/ToggleSwitch'; import ToggleSwitch from '@/components/ToggleSwitch';
import { createProfile, updateProfile } from '@/app/lib/authAction'; import { createProfile, updateProfile } from '@/app/lib/authAction';
import useCsrfToken from '@/hooks/useCsrfToken'; import { useCsrfToken } from '@/context/CsrfContext';
import { DndProvider, useDrag, useDrop } from 'react-dnd'; import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import InputText from '@/components/InputText'; import InputText from '@/components/InputText';

View File

@ -1,12 +0,0 @@
import { getCsrfToken } from '@/utils/getCsrfToken';
export const csrfMiddleware = (handler) => {
return async (req, res) => {
const csrfToken = getCsrfToken();
if (!csrfToken) {
console.error('CSRF Token is undefined');
}
req.csrfToken = csrfToken;
return handler(req, res);
};
};

View File

@ -1,29 +0,0 @@
// import { useEffect, useState } from 'react';
// import { BE_AUTH_CSRF_URL } from '@/utils/Url';
// const useCsrfToken = () => {
// const [token, setToken] = useState('');
// useEffect(() => {
// fetch(`${BE_AUTH_CSRF_URL}`, {
// method: 'GET',
// credentials: 'include' // Inclut les cookies dans la requête
// })
// .then(response => response.json())
// .then(data => {
// if (data) {
// if(data.csrfToken != token) {
// setToken(data.csrfToken);
// console.log('------------> CSRF Token reçu:', data.csrfToken);
// }
// }
// })
// .catch(error => {
// console.error('Error fetching CSRF token:', error);
// });
// }, []);
// return token;
// };
// export default useCsrfToken;

View File

@ -1,7 +1,6 @@
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 jwt from 'jsonwebtoken'; import { BE_AUTH_LOGIN_URL } from '@/utils/Url';
import { csrfMiddleware } from '@/csrfMiddleware'; // Importez le middleware csrfMiddleware
const options = { const options = {
providers: [ providers: [
@ -11,43 +10,32 @@ const options = {
email: { label: 'Email', type: 'email' }, email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' } password: { label: 'Password', type: 'password' }
}, },
authorize: (credentials, req) => { authorize: async (credentials, req) => {
console.log('Credentials:', credentials); // Vérifiez si ce log s'affiche const response = await fetch(`${BE_AUTH_LOGIN_URL}`, {
// Utilisez le token CSRF injecté par le middleware
const csrfToken = req.csrfToken;
console.log("data to send : ", JSON.stringify({
email: credentials.email,
password: credentials.password
}), "csrfToken : ", csrfToken);
return fetch(`${process.env.NEXT_PUBLIC_API_URL}/Auth/login`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-CSRFToken': csrfToken // Utiliser le token CSRF ici
}, },
body: JSON.stringify({ body: JSON.stringify({
email: credentials.email, email: credentials.email,
password: credentials.password password: credentials.password
}), }),
credentials: 'include' credentials: 'include'
}) });
.then(response => response.text())
.then(text => {
console.log('Response Text:', text); // Loggez la réponse
const user = JSON.parse(text); // Parsez la réponse en JSON
const user = await response.json();
console.log("API response:", user);
if (response.ok && user) { if (response.ok && user) {
return user; const userData = {
id: user.id,
role: user.profil,
droit: user.droit
};
return userData;
} else { } else {
throw new Error(user.errorMessage || 'Invalid credentials'); throw new Error(user.errorMessage || 'Invalid credentials');
} }
})
.catch(error => {
console.error('Error during authentication:', error);
throw new Error('Authentication failed');
});
} }
}) })
], ],
@ -55,25 +43,35 @@ const options = {
jwt: true jwt: true
}, },
callbacks: { callbacks: {
async jwt(token, user) { async jwt({ token, user }) {
console.log("JWT callback called", user);
if (user) { if (user) {
token.id = user.id; token.id = user.id;
token.email = user.email;
token.role = user.role; token.role = user.role;
token.droit = user.droit;
} }
return token; return token;
}, },
async session(session, token) { async session({ session, token }) {
session.user.id = token.id; console.log("Session callback called", token);
session.user.email = token.email; if (!token) {
session.user.role = token.role; throw new Error('Token not found');
}
session.user = {
id: token.id,
role: token.role,
droit: token.droit
};
return session; return session;
} }
}, },
pages: { pages: {
signIn: '/[locale]/users/login' signIn: '/[locale]/users/login'
}, },
csrf: false // Désactiver la gestion CSRF de NextAuth.js csrf: true
}; };
export default csrfMiddleware((req, res) => NextAuth(req, res, options)); export default (req, res) => {
console.log("NextAuth handler called");
return NextAuth(req, res, options);
};

View File

@ -1,42 +0,0 @@
import { useSession, getSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
export default function ProtectedPage() {
const [session, loading] = useSession();
const router = useRouter();
useEffect(() => {
if (!loading && !session) {
router.push('/auth/signin');
}
}, [loading, session, router]);
if (loading || !session) {
return <p>Loading...</p>;
}
return (
<div>
<h1>Protected Page</h1>
<p>Welcome, {session.user.email}</p>
</div>
);
}
export async function getServerSideProps(context) {
const session = await getSession(context);
if (!session) {
return {
redirect: {
destination: '/auth/signin',
permanent: false
}
};
}
return {
props: { session }
};
}