chore: ajustement JWT

This commit is contained in:
Luc SORIGNET
2025-02-22 10:52:50 +01:00
parent eb89a324ab
commit c861239d48
12 changed files with 244 additions and 75 deletions

View File

@ -2,12 +2,13 @@ from django.urls import path, re_path
from . import views from . import views
import Auth.views import Auth.views
from Auth.views import ProfileSimpleView, ProfileView, SessionView, LoginView, SubscribeView, NewPasswordView, ResetPasswordView from Auth.views import ProfileSimpleView, ProfileView, SessionView, LoginView, RefreshJWTView, SubscribeView, NewPasswordView, ResetPasswordView
urlpatterns = [ urlpatterns = [
re_path(r'^csrf$', Auth.views.csrf, name='csrf'), re_path(r'^csrf$', Auth.views.csrf, name='csrf'),
re_path(r'^login$', LoginView.as_view(), name="login"), re_path(r'^login$', LoginView.as_view(), name="login"),
re_path(r'^refreshJWT$', RefreshJWTView.as_view(), name="refresh_jwt"),
re_path(r'^subscribe$', SubscribeView.as_view(), name='subscribe'), re_path(r'^subscribe$', SubscribeView.as_view(), name='subscribe'),
re_path(r'^newPassword$', NewPasswordView.as_view(), name='newPassword'), re_path(r'^newPassword$', NewPasswordView.as_view(), name='newPassword'),
re_path(r'^resetPassword/(?P<code>[a-zA-Z]+)$', ResetPasswordView.as_view(), name='resetPassword'), re_path(r'^resetPassword/(?P<code>[a-zA-Z]+)$', ResetPasswordView.as_view(), name='resetPassword'),

View File

@ -13,8 +13,9 @@ from rest_framework import status
from drf_yasg.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi from drf_yasg import openapi
from datetime import datetime from datetime import datetime, timedelta
import jwt import jwt
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
import json import json
from . import validator from . import validator
@ -26,11 +27,13 @@ from Subscriptions.models import RegistrationForm
from Subscriptions.signals import clear_cache from Subscriptions.signals import clear_cache
import Subscriptions.mailManager as mailer import Subscriptions.mailManager as mailer
import Subscriptions.util as util import Subscriptions.util as util
import logging
from N3wtSchool import bdd, error from N3wtSchool import bdd, error
from rest_framework_simplejwt.authentication import JWTAuthentication from rest_framework_simplejwt.authentication import JWTAuthentication
logger = logging.getLogger("AuthViews")
@swagger_auto_schema( @swagger_auto_schema(
method='get', method='get',
@ -162,13 +165,17 @@ class LoginView(APIView):
), ),
responses={ responses={
200: openapi.Response('Connexion réussie', schema=openapi.Schema( 200: openapi.Response('Connexion réussie', schema=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'token': openapi.Schema(type=openapi.TYPE_STRING),
'refresh': openapi.Schema(type=openapi.TYPE_STRING)
}
)),
400: openapi.Response('Connexion échouée', schema=openapi.Schema(
type=openapi.TYPE_OBJECT, type=openapi.TYPE_OBJECT,
properties={ properties={
'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),
'droit': openapi.Schema(type=openapi.TYPE_INTEGER),
'id': openapi.Schema(type=openapi.TYPE_INTEGER),
} }
)) ))
} }
@ -179,6 +186,7 @@ class LoginView(APIView):
retour = error.returnMessage[error.WRONG_ID] retour = error.returnMessage[error.WRONG_ID]
validationOk, errorFields = validatorAuthentication.validate() validationOk, errorFields = validatorAuthentication.validate()
user = None user = None
if validationOk: if validationOk:
user = authenticate( user = authenticate(
email=data.get('email'), email=data.get('email'),
@ -191,6 +199,31 @@ class LoginView(APIView):
user.save() user.save()
clear_cache() clear_cache()
retour = '' retour = ''
# Générer le JWT avec la bonne syntaxe datetime
access_payload = {
'user_id': user.id,
'email': user.email,
'droit': user.droit,
'type': 'access',
'exp': datetime.utcnow() + settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'],
'iat': datetime.utcnow(),
}
access_token = jwt.encode(access_payload, settings.SIMPLE_JWT['SIGNING_KEY'], algorithm=settings.SIMPLE_JWT['ALGORITHM'])
# Générer le Refresh Token (exp: 7 jours)
refresh_payload = {
'user_id': user.id,
'type': 'refresh',
'exp': datetime.utcnow() + settings.SIMPLE_JWT['REFRESH_TOKEN_LIFETIME'],
'iat': datetime.utcnow(),
}
refresh_token = jwt.encode(refresh_payload, settings.SIMPLE_JWT['SIGNING_KEY'], algorithm=settings.SIMPLE_JWT['ALGORITHM'])
return JsonResponse({
'token': access_token,
'refresh': refresh_token
}, safe=False)
else: else:
retour = error.returnMessage[error.PROFIL_INACTIVE] retour = error.returnMessage[error.PROFIL_INACTIVE]
else: else:
@ -199,10 +232,101 @@ class LoginView(APIView):
return JsonResponse({ return JsonResponse({
'errorFields': errorFields, 'errorFields': errorFields,
'errorMessage': retour, 'errorMessage': retour,
'profil': user.id if user else -1, }, safe=False, status=status.HTTP_400_BAD_REQUEST)
'droit': user.droit if user else -1,
'id': user.id if user else -1,
}, safe=False) class RefreshJWTView(APIView):
@swagger_auto_schema(
operation_description="Rafraîchir le token d'accès",
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
required=['refresh'],
properties={
'refresh': openapi.Schema(type=openapi.TYPE_STRING)
}
),
responses={
200: openapi.Response('Token rafraîchi avec succès', schema=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'token': openapi.Schema(type=openapi.TYPE_STRING),
'refresh': openapi.Schema(type=openapi.TYPE_STRING),
}
)),
400: openapi.Response('Échec du rafraîchissement', schema=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'errorMessage': openapi.Schema(type=openapi.TYPE_STRING)
}
))
}
)
@method_decorator(csrf_exempt, name='dispatch')
def post(self, request):
data = JSONParser().parse(request)
refresh_token = data.get("refresh")
logger.info(f"Token reçu: {refresh_token[:20]}...") # Ne pas logger le token complet pour la sécurité
if not refresh_token:
return JsonResponse({'errorMessage': 'Refresh token manquant'}, status=400)
try:
# Décoder le Refresh Token
logger.info("Tentative de décodage du token")
logger.info(f"Algorithme utilisé: {settings.SIMPLE_JWT['ALGORITHM']}")
# Vérifier le format du token avant décodage
token_parts = refresh_token.split('.')
if len(token_parts) != 3:
logger.error("Format de token invalide - pas 3 parties")
return JsonResponse({'errorMessage': 'Format de token invalide'}, status=400)
payload = jwt.decode(
refresh_token,
settings.SIMPLE_JWT['SIGNING_KEY'],
algorithms=[settings.SIMPLE_JWT['ALGORITHM']] # Noter le passage en liste
)
logger.info(f"Token décodé avec succès. Type: {payload.get('type')}")
# Vérifier s'il s'agit bien d'un Refresh Token
if payload.get('type') != 'refresh':
return JsonResponse({'errorMessage': 'Token invalide'}, status=400)
# Récupérer les informations utilisateur
user = Profile.objects.get(id=payload['user_id'])
# Générer un nouveau Access Token avec les informations complètes
new_access_payload = {
'user_id': user.id,
'email': user.email,
'droit': user.droit,
'type': 'access',
'exp': datetime.utcnow() + settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'],
'iat': datetime.utcnow(),
}
new_access_token = jwt.encode(new_access_payload, settings.SIMPLE_JWT['SIGNING_KEY'], algorithm=settings.SIMPLE_JWT['ALGORITHM'])
new_refresh_payload = {
'user_id': user.id,
'type': 'refresh',
'exp': datetime.utcnow() + settings.SIMPLE_JWT['REFRESH_TOKEN_LIFETIME'],
'iat': datetime.utcnow(),
}
new_refresh_token = jwt.encode(new_refresh_payload, settings.SIMPLE_JWT['SIGNING_KEY'], algorithm=settings.SIMPLE_JWT['ALGORITHM'])
return JsonResponse({'token': new_access_token, 'refresh': new_refresh_token}, status=200)
except ExpiredSignatureError as e:
logger.error(f"Token expiré: {str(e)}")
return JsonResponse({'errorMessage': 'Refresh token expiré'}, status=400)
except InvalidTokenError as e:
logger.error(f"Token invalide: {str(e)}")
return JsonResponse({'errorMessage': f'Token invalide: {str(e)}'}, status=400)
except Exception as e:
logger.error(f"Erreur inattendue: {str(e)}")
return JsonResponse({'errorMessage': f'Erreur inattendue: {str(e)}'}, status=400)
@method_decorator(csrf_protect, name='dispatch') @method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch')

View File

@ -314,7 +314,7 @@ REDIS_PASSWORD = None
SECRET_KEY = 'QWQ8bYlCz1NpQ9G0vR5kxMnvWszfH2y3' SECRET_KEY = 'QWQ8bYlCz1NpQ9G0vR5kxMnvWszfH2y3'
SIMPLE_JWT = { SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1), 'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'ROTATE_REFRESH_TOKENS': False, 'ROTATE_REFRESH_TOKENS': False,
'BLACKLIST_AFTER_ROTATION': True, 'BLACKLIST_AFTER_ROTATION': True,

View File

View File

@ -8,6 +8,9 @@ const nextConfig = {
output: "standalone", output: "standalone",
experimental: { experimental: {
instrumentationHook: true, instrumentationHook: true,
},
images: {
domains:['i.pravatar.cc'],
}, },
env: { env: {
NEXT_PUBLIC_APP_VERSION: pkg.version, NEXT_PUBLIC_APP_VERSION: pkg.version,

View File

@ -4,6 +4,7 @@ import React, { useState, useEffect } from 'react';
import Sidebar from '@/components/Sidebar'; import Sidebar from '@/components/Sidebar';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import {useTranslations} from 'next-intl'; import {useTranslations} from 'next-intl';
import Image from 'next/image';
import { import {
Users, Users,
Building, Building,
@ -94,7 +95,7 @@ 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={<img src="https://i.pravatar.cc/32" alt="Profile" className="w-8 h-8 rounded-full cursor-pointer" />} buttonContent={<Image src="https://i.pravatar.cc/32" alt="Profile" className="w-8 h-8 rounded-full cursor-pointer" width={150} height={150} />}
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"

View File

@ -1,6 +1,7 @@
'use client' 'use client'
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';
const contacts = [ const contacts = [
{ id: 1, name: 'Facturation', profilePic: 'https://i.pravatar.cc/32' }, { id: 1, name: 'Facturation', profilePic: 'https://i.pravatar.cc/32' },
@ -61,7 +62,7 @@ export default function MessageriePage() {
className={`p-2 cursor-pointer ${selectedContact?.id === contact.id ? 'bg-gray-200' : ''}`} className={`p-2 cursor-pointer ${selectedContact?.id === contact.id ? 'bg-gray-200' : ''}`}
onClick={() => setSelectedContact(contact)} onClick={() => setSelectedContact(contact)}
> >
<img src={contact.profilePic} alt={`${contact.name}'s profile`} className="w-8 h-8 rounded-full inline-block mr-2" /> <Image src={contact.profilePic} alt={`${contact.name}'s profile`} className="w-8 h-8 rounded-full inline-block mr-2" width={150} height={150}/>
{contact.name} {contact.name}
</div> </div>
))} ))}
@ -75,7 +76,7 @@ export default function MessageriePage() {
style={{ borderRadius: message.isResponse ? '20px 20px 0 20px' : '20px 20px 20px 0', minWidth: '25%' }} style={{ borderRadius: message.isResponse ? '20px 20px 0 20px' : '20px 20px 20px 0', minWidth: '25%' }}
> >
<div className="flex items-center mb-1"> <div className="flex items-center mb-1">
<img src={selectedContact.profilePic} alt={`${selectedContact.name}'s profile`} className="w-8 h-8 rounded-full inline-block mr-2" /> <img src={selectedContact.profilePic} alt={`${selectedContact.name}'s profile`} className="w-8 h-8 rounded-full inline-block mr-2" width={150} height={150} />
<span className="text-xs text-gray-600">{selectedContact.name}</span> <span className="text-xs text-gray-600">{selectedContact.name}</span>
<span className="text-xs text-gray-400 ml-2">{new Date(message.date).toLocaleTimeString()}</span> <span className="text-xs text-gray-400 ml-2">{new Date(message.date).toLocaleTimeString()}</span>
</div> </div>

View File

@ -4,8 +4,8 @@ import logoImage from '@/img/logo_min.svg'; // Assurez-vous que le chemin vers l
const Logo = ({ className }) => { const Logo = ({ className }) => {
return ( return (
<div className={className}> <div className={`max-w-[150px] ${className}`}>
<Image src={logoImage} alt="Logo" width={150} height={150} /> <Image src={logoImage} alt="Logo" style={{ width: 'auto', height: 'auto'}} priority />
</div> </div>
); );
}; };

View File

@ -5,23 +5,21 @@ 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 { data: session, status } = useSession({
const router = useRouter(); required: true,
onUnauthenticated() {
useEffect(() => {
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é
router.push(`${FE_USERS_LOGIN_URL}`); router.push(`${FE_USERS_LOGIN_URL}`);
} }
}, [session, status, router]); });
const router = useRouter();
if (status === 'loading' || !session) { // Ne vérifier que si le statut est définitif
return <Loader />; // Affichez un loader pendant le chargement ou si l'utilisateur n'est pas connecté if (status === 'loading') {
return <Loader />;
} }
// Afficher les enfants seulement si l'utilisateur est connecté // Autoriser l'affichage si authentifié
return children; return session ? children : null;
}; };
export default ProtectedRoute; export default ProtectedRoute;

View File

@ -1,31 +1,48 @@
import { useState } from "react" 'use client';
import { useState, useEffect } from 'react';
const useLocalStorage = (key, initialValue) => { const useLocalStorage = (key, initialValue) => {
const [state, setState] = useState(() => { const [storedValue, setStoredValue] = useState(() => {
// Initialize the state
try { try {
const value = window.localStorage.getItem(key) if (typeof window !== 'undefined') {
// Check if the local storage already has any values, const item = window.localStorage.getItem(key);
// otherwise initialize it with the passed initialValue // Vérifier si l'item existe et n'est pas undefined
return value ? JSON.parse(value) : initialValue return item !== null && item !== 'undefined'
} catch (error) { ? JSON.parse(item)
console.log(error) : initialValue;
} }
}) return initialValue;
} catch (error) {
console.error('Error reading from localStorage:', error);
return initialValue;
}
});
const setValue = value => { useEffect(() => {
try { try {
// If the passed value is a callback function, // Vérifier si la valeur n'est pas undefined avant de la stocker
// then call it with the existing state. if (typeof storedValue !== 'undefined') {
const valueToStore = value instanceof Function ? value(state) : value window.localStorage.setItem(key, JSON.stringify(storedValue));
window.localStorage.setItem(key, JSON.stringify(valueToStore)) } else {
setState(value) window.localStorage.removeItem(key);
}
} catch (error) { } catch (error) {
console.log(error) console.error('Error writing to localStorage:', error);
}
} }
}, [key, storedValue]);
return [state, setValue] const setValue = (value) => {
try {
// Permettre à la valeur d'être une fonction
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
} catch (error) {
console.error('Error updating localStorage value:', error);
} }
};
export default useLocalStorage return [storedValue, setValue];
};
export default useLocalStorage;

View File

@ -35,45 +35,69 @@ const options = {
], ],
session: { session: {
strategy: "jwt", strategy: "jwt",
maxAge: 30 * 24 * 60 * 60, // 30 jours
updateAge: 24 * 60 * 60, // 24 heures
},
cookies: {
sessionToken: {
name: 'n3wtschool_session_token',
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: process.env.NODE_ENV === 'production'
}
}
}, },
callbacks: { callbacks: {
async jwt({ token, user }) { async jwt({ token, user, trigger }) {
// Si c'est la première connexion
if (user) { if (user) {
token.token = user.token; return {
token.refresh = user.refresh; ...token,
token.tokenExpires = jwt_decode.decode(user.token).exp * 1000; token: user.token,
refresh: user.refresh,
tokenExpires: jwt_decode.decode(user.token).exp * 1000
};
} }
// Vérifie si l'access token a expiré
// Vérifier si le token n'est pas expiré
if (Date.now() < token.tokenExpires) { if (Date.now() < token.tokenExpires) {
return token; return token;
} }
// Renouvelle le token expiré
// Token expiré, essayer de le rafraîchir
try { try {
const data = {refresh: token.refresh} const response = await refreshJWT({ refresh: token.refresh });
const res = await refreshJWT(data); if (!response) {
console.log(res); throw new Error('Failed to refresh token');
token.token = res.token; }
token.refresh = res.refresh;
token.tokenExpires = jwt_decode.decode(res.token).exp * 1000; return {
console.log("Token refreshed", token); ...token,
return token; token: response.token,
refresh: response.refresh,
tokenExpires: jwt_decode.decode(response.token).exp * 1000
};
} catch (error) { } catch (error) {
console.error("Erreur lors du rafraîchissement du token", error); console.error("Refresh token failed:", error);
return token; return token;
} }
}, },
async session({ session, token }) { async session({ session, token }) {
console.log("Session callback called", token); if (token) {
if (!token) { const {user_id, droit, email} = jwt_decode.decode(token.token);
throw new Error('Token not found'); session.user = {
...session.user,
token: token.token,
refresh: token.refresh
};
session.user.user_id = user_id;
session.user.droit = droit;
session.user.email = email;
} }
else{
const decodedToken = jwt_decode.decode(token.token);
const {user_id,email,droit} = decodedToken;
session.user = {id:user_id,email,droit};
return session; return session;
} }
}
}, },
pages: { pages: {
signIn: '/[locale]/users/login' signIn: '/[locale]/users/login'