mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-03 16:51:26 +00:00
feat: Securisation du Backend
This commit is contained in:
@ -17,10 +17,12 @@ from datetime import datetime, timedelta
|
||||
import jwt
|
||||
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from . import validator
|
||||
from .models import Profile, ProfileRole
|
||||
from rest_framework.decorators import action, api_view
|
||||
from rest_framework.decorators import action, api_view, permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from django.db.models import Q
|
||||
|
||||
from Auth.serializers import ProfileSerializer, ProfileRoleSerializer
|
||||
@ -28,13 +30,28 @@ from Subscriptions.models import RegistrationForm, Guardian
|
||||
import N3wtSchool.mailManager as mailer
|
||||
import Subscriptions.util as util
|
||||
import logging
|
||||
from N3wtSchool import bdd, error, settings
|
||||
from N3wtSchool import bdd, error
|
||||
|
||||
from rest_framework.throttling import AnonRateThrottle
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
|
||||
logger = logging.getLogger("AuthViews")
|
||||
|
||||
|
||||
class LoginRateThrottle(AnonRateThrottle):
|
||||
"""Limite les tentatives de connexion à 10/min par IP.
|
||||
Configurable via DEFAULT_THROTTLE_RATES['login'] dans settings.
|
||||
"""
|
||||
scope = 'login'
|
||||
|
||||
def get_rate(self):
|
||||
try:
|
||||
return super().get_rate()
|
||||
except Exception:
|
||||
# Fallback si le scope 'login' n'est pas configuré dans les settings
|
||||
return '10/min'
|
||||
|
||||
|
||||
@swagger_auto_schema(
|
||||
method='get',
|
||||
operation_description="Obtenir un token CSRF",
|
||||
@ -43,11 +60,15 @@ logger = logging.getLogger("AuthViews")
|
||||
}))}
|
||||
)
|
||||
@api_view(['GET'])
|
||||
@permission_classes([AllowAny])
|
||||
def csrf(request):
|
||||
token = get_token(request)
|
||||
return JsonResponse({'csrfToken': token})
|
||||
|
||||
class SessionView(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
authentication_classes = [] # SessionView gère sa propre validation JWT
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Vérifier une session utilisateur",
|
||||
manual_parameters=[openapi.Parameter('Authorization', openapi.IN_HEADER, type=openapi.TYPE_STRING, description='Bearer token')],
|
||||
@ -70,6 +91,11 @@ class SessionView(APIView):
|
||||
token = request.META.get('HTTP_AUTHORIZATION', '').split('Bearer ')[-1]
|
||||
try:
|
||||
decoded_token = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256'])
|
||||
# Refuser les refresh tokens : seul le type 'access' est autorisé
|
||||
# Accepter 'type' (format custom) ET 'token_type' (format SimpleJWT)
|
||||
token_type_claim = decoded_token.get('type') or decoded_token.get('token_type')
|
||||
if token_type_claim != 'access':
|
||||
return JsonResponse({"error": "Invalid token"}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
userid = decoded_token.get('user_id')
|
||||
user = Profile.objects.get(id=userid)
|
||||
roles = ProfileRole.objects.filter(profile=user).values('role_type', 'establishment__name')
|
||||
@ -88,6 +114,8 @@ class SessionView(APIView):
|
||||
return JsonResponse({"error": "Invalid token"}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
class ProfileView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Obtenir la liste des profils",
|
||||
responses={200: ProfileSerializer(many=True)}
|
||||
@ -118,6 +146,8 @@ class ProfileView(APIView):
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||
class ProfileSimpleView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Obtenir un profil par son ID",
|
||||
responses={200: ProfileSerializer}
|
||||
@ -152,8 +182,12 @@ class ProfileSimpleView(APIView):
|
||||
def delete(self, request, id):
|
||||
return bdd.delete_object(Profile, id)
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||
class LoginView(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
throttle_classes = [LoginRateThrottle]
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Connexion utilisateur",
|
||||
request_body=openapi.Schema(
|
||||
@ -240,12 +274,14 @@ def makeToken(user):
|
||||
})
|
||||
|
||||
# Générer le JWT avec la bonne syntaxe datetime
|
||||
# jti (JWT ID) est obligatoire : SimpleJWT le vérifie via AccessToken.verify_token_id()
|
||||
access_payload = {
|
||||
'user_id': user.id,
|
||||
'email': user.email,
|
||||
'roleIndexLoginDefault': user.roleIndexLoginDefault,
|
||||
'roles': roles,
|
||||
'type': 'access',
|
||||
'jti': str(uuid.uuid4()),
|
||||
'exp': datetime.utcnow() + settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'],
|
||||
'iat': datetime.utcnow(),
|
||||
}
|
||||
@ -255,16 +291,23 @@ def makeToken(user):
|
||||
refresh_payload = {
|
||||
'user_id': user.id,
|
||||
'type': 'refresh',
|
||||
'jti': str(uuid.uuid4()),
|
||||
'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 access_token, refresh_token
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la création du token: {str(e)}")
|
||||
return None
|
||||
logger.error(f"Erreur lors de la création du token: {str(e)}", exc_info=True)
|
||||
# On lève l'exception pour que l'appelant (LoginView / RefreshJWTView)
|
||||
# retourne une erreur HTTP 500 explicite plutôt que de crasher silencieusement
|
||||
# sur le unpack d'un None.
|
||||
raise
|
||||
|
||||
class RefreshJWTView(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
throttle_classes = [LoginRateThrottle]
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Rafraîchir le token d'accès",
|
||||
request_body=openapi.Schema(
|
||||
@ -290,7 +333,6 @@ class RefreshJWTView(APIView):
|
||||
))
|
||||
}
|
||||
)
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
def post(self, request):
|
||||
data = JSONParser().parse(request)
|
||||
refresh_token = data.get("refresh")
|
||||
@ -335,14 +377,16 @@ class RefreshJWTView(APIView):
|
||||
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)
|
||||
return JsonResponse({'errorMessage': 'Token invalide'}, status=400)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur inattendue: {str(e)}")
|
||||
return JsonResponse({'errorMessage': f'Erreur inattendue: {str(e)}'}, status=400)
|
||||
logger.error(f"Erreur inattendue: {str(e)}", exc_info=True)
|
||||
return JsonResponse({'errorMessage': 'Erreur interne du serveur'}, status=500)
|
||||
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||
class SubscribeView(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Inscription utilisateur",
|
||||
manual_parameters=[
|
||||
@ -430,6 +474,8 @@ class SubscribeView(APIView):
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||
class NewPasswordView(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Demande de nouveau mot de passe",
|
||||
request_body=openapi.Schema(
|
||||
@ -479,6 +525,8 @@ class NewPasswordView(APIView):
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||
class ResetPasswordView(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Réinitialisation du mot de passe",
|
||||
request_body=openapi.Schema(
|
||||
@ -525,7 +573,9 @@ class ResetPasswordView(APIView):
|
||||
return JsonResponse({'message': retour, "errorMessage": retourErreur, "errorFields": errorFields}, safe=False)
|
||||
|
||||
class ProfileRoleView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
pagination_class = CustomProfilesPagination
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Obtenir la liste des profile_roles",
|
||||
responses={200: ProfileRoleSerializer(many=True)}
|
||||
@ -596,6 +646,8 @@ class ProfileRoleView(APIView):
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||
class ProfileRoleSimpleView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Obtenir un profile_role par son ID",
|
||||
responses={200: ProfileRoleSerializer}
|
||||
|
||||
Reference in New Issue
Block a user