feat: Gestion multi-profil multi-école

This commit is contained in:
N3WT DE COMPET
2025-03-09 16:22:28 +01:00
parent 95c154a4a2
commit 16178296ec
51 changed files with 1621 additions and 802 deletions

View File

@ -2,25 +2,31 @@ from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.core.validators import EmailValidator
from Establishment.models import Establishment
class Profile(AbstractUser):
class Droits(models.IntegerChoices):
PROFIL_UNDEFINED = -1, _('NON DEFINI')
PROFIL_ECOLE = 0, _('ECOLE')
PROFIL_ADMIN = 1, _('ADMIN')
PROFIL_PARENT = 2, _('PARENT')
email = models.EmailField(max_length=255, unique=True, default="", validators=[EmailValidator()])
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ('password', )
code = models.CharField(max_length=200, default="", blank=True)
datePeremption = models.CharField(max_length=200, default="", blank=True)
droit = models.IntegerField(choices=Droits, default=Droits.PROFIL_UNDEFINED)
estConnecte = models.BooleanField(default=False, blank=True)
establishment = models.ForeignKey('School.Establishment', on_delete=models.PROTECT, related_name='profile', null=True, blank=True)
def __str__(self):
return self.email + " - " + str(self.droit)
return self.email
class ProfileRole(models.Model):
class RoleType(models.IntegerChoices):
PROFIL_UNDEFINED = -1, _('NON DEFINI')
PROFIL_ECOLE = 0, _('ECOLE')
PROFIL_ADMIN = 1, _('ADMIN')
PROFIL_PARENT = 2, _('PARENT')
profile = models.ForeignKey(Profile, on_delete=models.CASCADE, related_name='roles')
role_type = models.IntegerField(choices=RoleType.choices, default=RoleType.PROFIL_UNDEFINED)
establishment = models.ForeignKey(Establishment, on_delete=models.CASCADE, related_name='profile_roles')
is_active = models.BooleanField(default=False)
def __str__(self):
return f"{self.profile.email} - {self.get_role_type_display()} - {self.establishment.name}"

View File

@ -1,48 +1,97 @@
from rest_framework import serializers
from Auth.models import Profile
from Auth.models import Profile, ProfileRole
from django.core.exceptions import ValidationError
class ProfileRoleSerializer(serializers.ModelSerializer):
class Meta:
model = ProfileRole
fields = ['role_type', 'establishment', 'is_active', 'profile']
class ProfileSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
password = serializers.CharField(write_only=True)
roles = ProfileRoleSerializer(many=True, required=False)
class Meta:
model = Profile
fields = ['id', 'password', 'email', 'code', 'datePeremption', 'estConnecte', 'droit', 'username', 'is_active', 'establishment']
fields = ['id', 'password', 'email', 'code', 'datePeremption', 'username', 'roles']
extra_kwargs = {'password': {'write_only': True}}
def create(self, validated_data):
roles_data = validated_data.pop('roles', [])
user = Profile(
username=validated_data['username'],
email=validated_data['email'],
is_active=validated_data['is_active'],
droit=validated_data['droit'],
establishment=validated_data.get('establishment')
code=validated_data.get('code', ''),
datePeremption=validated_data.get('datePeremption', '')
)
user.set_password(validated_data['password'])
user.full_clean()
user.save()
for role_data in roles_data:
ProfileRole.objects.create(profile=user, **role_data)
return user
def to_representation(self, instance):
ret = super().to_representation(instance)
ret['password'] = '********'
return ret
class ProfilUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
fields = ['id', 'password', 'email', 'code', 'datePeremption', 'estConnecte', 'droit', 'username', 'is_active']
extra_kwargs = {
'password': {'write_only': True, 'required': False}
}
def update(self, instance, validated_data):
roles_data = validated_data.pop('roles', [])
password = validated_data.pop('password', None)
instance = super().update(instance, validated_data)
if password:
instance.set_password(password)
instance.save()
instance.full_clean()
instance.save()
for role_data in roles_data:
ProfileRole.objects.update_or_create(
profile=instance,
establishment_id=role_data.get('establishment_id'),
defaults={
'role_type': role_data.get('role_type'),
'is_active': role_data.get('is_active', True)
}
)
return instance
def to_representation(self, instance):
ret = super().to_representation(instance)
ret['password'] = '********'
return ret
class ProfilUpdateSerializer(serializers.ModelSerializer):
roles = ProfileRoleSerializer(many=True, required=False)
class Meta:
model = Profile
fields = ['id', 'password', 'email', 'code', 'datePeremption', 'username', 'roles']
extra_kwargs = {
'password': {'write_only': True, 'required': False}
}
def update(self, instance, validated_data):
roles_data = validated_data.pop('roles', [])
password = validated_data.pop('password', None)
instance = super().update(instance, validated_data)
if password:
instance.set_password(password)
instance.full_clean()
instance.save()
for role_data in roles_data:
ProfileRole.objects.update_or_create(
profile=instance,
establishment_id=role_data.get('establishment_id'),
defaults={
'role_type': role_data.get('role_type'),
'is_active': role_data.get('is_active', True)
}
)
return instance

View File

@ -19,7 +19,7 @@ from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
import json
from . import validator
from .models import Profile
from .models import Profile, ProfileRole
from rest_framework.decorators import action, api_view
from Auth.serializers import ProfileSerializer, ProfilUpdateSerializer
@ -56,7 +56,10 @@ class SessionView(APIView):
'user': openapi.Schema(type=openapi.TYPE_OBJECT, properties={
'id': openapi.Schema(type=openapi.TYPE_INTEGER),
'email': openapi.Schema(type=openapi.TYPE_STRING),
'role': openapi.Schema(type=openapi.TYPE_STRING)
'roles': openapi.Schema(type=openapi.TYPE_ARRAY, items=openapi.Items(type=openapi.TYPE_OBJECT, properties={
'role_type': openapi.Schema(type=openapi.TYPE_STRING),
'establishment': openapi.Schema(type=openapi.TYPE_STRING)
}))
})
})),
401: openapi.Response('Session invalide')
@ -67,15 +70,16 @@ class SessionView(APIView):
try:
decoded_token = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256'])
print(f'decode : {decoded_token}')
userid = decoded_token.get('id')
userid = decoded_token.get('user_id')
user = Profile.objects.get(id=userid)
roles = ProfileRole.objects.filter(profile=user).values('role_type', 'establishment__name')
response_data = {
'user': {
'id': user.id,
'email': user.email,
'role': user.droit, # Assure-toi que le champ 'droit' existe et contient le rôle
'roles': list(roles)
}
}
return JsonResponse(response_data, status=status.HTTP_200_OK)
@ -103,13 +107,11 @@ class ProfileView(APIView):
}
)
def post(self, request):
profil_data=JSONParser().parse(request)
print(f'{profil_data}')
profil_data = JSONParser().parse(request)
profil_serializer = ProfileSerializer(data=profil_data)
if profil_serializer.is_valid():
profil_serializer.save()
profil = profil_serializer.save()
return JsonResponse(profil_serializer.data, safe=False)
return JsonResponse(profil_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
@ -122,8 +124,8 @@ class ProfileSimpleView(APIView):
responses={200: ProfileSerializer}
)
def get(self, request, id):
profil=bdd.getObject(Profile, "id", id)
profil_serializer=ProfileSerializer(profil)
profil = bdd.getObject(Profile, "id", id)
profil_serializer = ProfileSerializer(profil)
return JsonResponse(profil_serializer.data, safe=False)
@swagger_auto_schema(
@ -135,12 +137,12 @@ class ProfileSimpleView(APIView):
}
)
def put(self, request, id):
data=JSONParser().parse(request)
data = JSONParser().parse(request)
profil = Profile.objects.get(id=id)
profil_serializer = ProfilUpdateSerializer(profil, data=data)
if profil_serializer.is_valid():
profil_serializer.save()
return JsonResponse("Updated Successfully", safe=False)
return JsonResponse(profil_serializer.data, safe=False)
return JsonResponse(profil_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
@ -157,10 +159,11 @@ class LoginView(APIView):
operation_description="Connexion utilisateur",
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
required=['email', 'password'],
required=['email', 'password', 'role_type'],
properties={
'email': openapi.Schema(type=openapi.TYPE_STRING),
'password': openapi.Schema(type=openapi.TYPE_STRING)
'password': openapi.Schema(type=openapi.TYPE_STRING),
'role_type': openapi.Schema(type=openapi.TYPE_STRING)
}
),
responses={
@ -193,40 +196,45 @@ class LoginView(APIView):
password=data.get('password'),
)
if user is not None:
if user.is_active:
login(request, user)
user.estConnecte = True
user.save()
clear_cache()
retour = ''
# Générer le JWT avec la bonne syntaxe datetime
access_payload = {
'user_id': user.id,
'email': user.email,
'droit': user.droit,
'establishment': user.establishment.id,
'type': 'access',
'exp': datetime.utcnow() + settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'],
'iat': datetime.utcnow(),
}
role_type = data.get('role_type')
primary_role = ProfileRole.objects.filter(profile=user, role_type=role_type, is_active=True).first()
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'])
if not primary_role:
return JsonResponse({"errorMessage": "Role not assigned to the user"}, status=status.HTTP_401_UNAUTHORIZED)
return JsonResponse({
'token': access_token,
'refresh': refresh_token
}, safe=False)
login(request, user)
user.save()
clear_cache()
retour = ''
# Récupérer tous les rôles de l'utilisateur avec le type spécifié
roles = ProfileRole.objects.filter(profile=user, role_type=role_type).values('role_type', 'establishment__id', 'establishment__name')
# Générer le JWT avec la bonne syntaxe datetime
access_payload = {
'user_id': user.id,
'email': user.email,
'roles': list(roles),
'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:
retour = error.returnMessage[error.PROFIL_INACTIVE]
else:
retour = error.returnMessage[error.WRONG_ID]
@ -235,7 +243,6 @@ class LoginView(APIView):
'errorMessage': retour,
}, safe=False, status=status.HTTP_400_BAD_REQUEST)
class RefreshJWTView(APIView):
@swagger_auto_schema(
operation_description="Rafraîchir le token d'accès",
@ -295,13 +302,20 @@ class RefreshJWTView(APIView):
# Récupérer les informations utilisateur
user = Profile.objects.get(id=payload['user_id'])
role_type = payload.get('role_type')
# Récupérer le rôle principal de l'utilisateur
primary_role = ProfileRole.objects.filter(profile=user, role_type=role_type).first()
if not primary_role:
return JsonResponse({'errorMessage': 'No role assigned to the user'}, status=400)
# 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,
'establishment': user.establishment.id,
'role_type': primary_role.get_role_type_display(),
'establishment': primary_role.establishment.id,
'type': 'access',
'exp': datetime.utcnow() + settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'],
'iat': datetime.utcnow(),
@ -311,6 +325,7 @@ class RefreshJWTView(APIView):
new_refresh_payload = {
'user_id': user.id,
'role_type': role_type,
'type': 'refresh',
'exp': datetime.utcnow() + settings.SIMPLE_JWT['REFRESH_TOKEN_LIFETIME'],
'iat': datetime.utcnow(),
@ -335,6 +350,14 @@ class RefreshJWTView(APIView):
class SubscribeView(APIView):
@swagger_auto_schema(
operation_description="Inscription utilisateur",
manual_parameters=[
openapi.Parameter(
'establishment_id', openapi.IN_QUERY,
description="ID de l'établissement",
type=openapi.TYPE_INTEGER,
required=True
)
],
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
required=['email', 'password1', 'password2'],
@ -359,37 +382,54 @@ class SubscribeView(APIView):
def post(self, request):
retourErreur = error.returnMessage[error.BAD_URL]
retour = ''
newProfilConnection=JSONParser().parse(request)
newProfilConnection = JSONParser().parse(request)
establishment_id = request.GET.get('establishment_id')
if not establishment_id:
return JsonResponse({'message': retour, 'errorMessage': 'establishment_id manquant', "errorFields": {}, "id": -1}, safe=False, status=status.HTTP_400_BAD_REQUEST)
validatorSubscription = validator.ValidatorSubscription(data=newProfilConnection)
validationOk, errorFields = validatorSubscription.validate()
if validationOk:
# On vérifie que l'email existe : si ce n'est pas le cas, on retourne une erreur
profil = bdd.getProfile(Profile.objects.all(), newProfilConnection.get('email'))
if profil == None:
if profil is None:
retourErreur = error.returnMessage[error.PROFIL_NOT_EXISTS]
else:
if profil.is_active:
retourErreur=error.returnMessage[error.PROFIL_ACTIVE]
return JsonResponse({'message':retour,'errorMessage':retourErreur, "errorFields":errorFields, "id":profil.id}, safe=False)
# Vérifier si le profil a déjà un rôle actif pour l'établissement donné
active_roles = ProfileRole.objects.filter(profile=profil, establishment_id=establishment_id, is_active=True)
if active_roles.exists():
retourErreur = error.returnMessage[error.PROFIL_ACTIVE]
return JsonResponse({'message': retour, 'errorMessage': retourErreur, "errorFields": errorFields, "id": profil.id}, safe=False)
else:
try:
profil.set_password(newProfilConnection.get('password1'))
profil.is_active = True
profil.full_clean()
profil.save()
# Utiliser le sérialiseur ProfileRoleSerializer pour créer ou mettre à jour le rôle
role_data = {
'profile': profil.id,
'establishment_id': establishment_id,
'role_type': ProfileRole.RoleType.PROFIL_PARENT,
'is_active': True
}
role_serializer = ProfileRoleSerializer(data=role_data)
if role_serializer.is_valid():
role_serializer.save()
else:
return JsonResponse(role_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
clear_cache()
retour = error.returnMessage[error.MESSAGE_ACTIVATION_PROFILE]
retourErreur=''
return JsonResponse({'message':retour,'errorMessage':retourErreur, "errorFields":errorFields, "id":profil.id}, safe=False)
retourErreur = ''
return JsonResponse({'message': retour, 'errorMessage': retourErreur, "errorFields": errorFields, "id": profil.id}, safe=False)
except ValidationError as e:
retourErreur = error.returnMessage[error.WRONG_MAIL_FORMAT]
return JsonResponse({'message':retour,'errorMessage':retourErreur, "errorFields":errorFields}, safe=False)
return JsonResponse({'message': retour, 'errorMessage': retourErreur, "errorFields": errorFields}, safe=False)
return JsonResponse({'message':retour, 'errorMessage':retourErreur, "errorFields":errorFields, "id":-1}, safe=False)
return JsonResponse({'message': retour, 'errorMessage': retourErreur, "errorFields": errorFields, "id": -1}, safe=False)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
@ -417,26 +457,29 @@ class NewPasswordView(APIView):
def post(self, request):
retourErreur = error.returnMessage[error.BAD_URL]
retour = ''
newProfilConnection=JSONParser().parse(request)
newProfilConnection = JSONParser().parse(request)
validatorNewPassword = validator.ValidatorNewPassword(data=newProfilConnection)
validationOk, errorFields = validatorNewPassword.validate()
if validationOk:
profil = bdd.getProfile(Profile.objects.all(), newProfilConnection.get('email'))
if profil == None:
if profil is None:
retourErreur = error.returnMessage[error.PROFIL_NOT_EXISTS]
else:
# Génération d'une URL provisoire pour modifier le mot de passe
profil.code = util.genereRandomCode(12)
profil.datePeremption = util.calculeDatePeremption(util._now(), settings.EXPIRATION_URL_NB_DAYS)
profil.save()
clear_cache()
retourErreur = ''
retour = error.returnMessage[error.MESSAGE_REINIT_PASSWORD]%(newProfilConnection.get('email'))
mailer.envoieReinitMotDePasse(newProfilConnection.get('email'), profil.code)
try:
# Génération d'une URL provisoire pour modifier le mot de passe
profil.code = util.genereRandomCode(12)
profil.datePeremption = util.calculeDatePeremption(util._now(), settings.EXPIRATION_URL_NB_DAYS)
profil.save()
clear_cache()
retourErreur = ''
retour = error.returnMessage[error.MESSAGE_REINIT_PASSWORD] % (newProfilConnection.get('email'))
mailer.envoieReinitMotDePasse(newProfilConnection.get('email'), profil.code)
except ValidationError as e:
retourErreur = error.returnMessage[error.WRONG_MAIL_FORMAT]
return JsonResponse({'message': retour, 'errorMessage': retourErreur, "errorFields": errorFields}, safe=False)
return JsonResponse({'message':retour, 'errorMessage':retourErreur, "errorFields":errorFields}, safe=False)
return JsonResponse({'message': retour, 'errorMessage': retourErreur, "errorFields": errorFields}, safe=False)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
@ -465,7 +508,7 @@ class ResetPasswordView(APIView):
def post(self, request, code):
retourErreur = error.returnMessage[error.BAD_URL]
retour = ''
newProfilConnection=JSONParser().parse(request)
newProfilConnection = JSONParser().parse(request)
validatorResetPassword = validator.ValidatorResetPassword(data=newProfilConnection)
validationOk, errorFields = validatorResetPassword.validate()
@ -474,16 +517,15 @@ class ResetPasswordView(APIView):
if profil:
if datetime.strptime(util.convertToStr(util._now(), '%d-%m-%Y %H:%M'), '%d-%m-%Y %H:%M') > datetime.strptime(profil.datePeremption, '%d-%m-%Y %H:%M'):
retourErreur = error.returnMessage[error.EXPIRED_URL]%(_uuid)
retourErreur = error.returnMessage[error.EXPIRED_URL] % (_uuid)
elif validationOk:
retour = error.returnMessage[error.PASSWORD_CHANGED]
profil.set_password(newProfilConnection.get('password1'))
profil.code = ''
profil.datePeremption = ''
profil.is_active = True
profil.save()
clear_cache()
retourErreur=''
retourErreur = ''
return JsonResponse({'message':retour, "errorMessage":retourErreur, "errorFields":errorFields}, safe=False)
return JsonResponse({'message': retour, "errorMessage": retourErreur, "errorFields": errorFields}, safe=False)