mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-03 16:51:26 +00:00
Merge pull request 'NEWTS-12 : Sécurisation Backend' (!72) from NEWTS12-Securisation_Backend into develop
Reviewed-on: https://git.v0id.ovh/n3wt-innov/n3wt-school/pulls/72
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
hardcoded-strings-report.md
|
hardcoded-strings-report.md
|
||||||
backend.env
|
backend.env
|
||||||
|
*.log
|
||||||
@ -2,6 +2,12 @@ from django.contrib.auth import get_user_model
|
|||||||
from django.contrib.auth.backends import ModelBackend
|
from django.contrib.auth.backends import ModelBackend
|
||||||
from Auth.models import Profile
|
from Auth.models import Profile
|
||||||
from N3wtSchool import bdd
|
from N3wtSchool import bdd
|
||||||
|
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||||
|
from rest_framework_simplejwt.exceptions import TokenError, InvalidToken
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger("Auth")
|
||||||
|
|
||||||
|
|
||||||
class EmailBackend(ModelBackend):
|
class EmailBackend(ModelBackend):
|
||||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||||
@ -18,3 +24,45 @@ class EmailBackend(ModelBackend):
|
|||||||
except Profile.DoesNotExist:
|
except Profile.DoesNotExist:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class LoggingJWTAuthentication(JWTAuthentication):
|
||||||
|
"""
|
||||||
|
Surclasse JWTAuthentication pour loguer pourquoi un token Bearer est rejeté.
|
||||||
|
Cela aide à diagnostiquer les 401 sans avoir à ajouter des prints partout.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def authenticate(self, request):
|
||||||
|
header = self.get_header(request)
|
||||||
|
if header is None:
|
||||||
|
logger.debug("JWT: pas de header Authorization dans la requête %s %s",
|
||||||
|
request.method, request.path)
|
||||||
|
return None
|
||||||
|
|
||||||
|
raw_token = self.get_raw_token(header)
|
||||||
|
if raw_token is None:
|
||||||
|
logger.debug("JWT: header Authorization présent mais token vide pour %s %s",
|
||||||
|
request.method, request.path)
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
validated_token = self.get_validated_token(raw_token)
|
||||||
|
except InvalidToken as e:
|
||||||
|
logger.warning(
|
||||||
|
"JWT: token invalide pour %s %s — %s",
|
||||||
|
request.method, request.path, str(e)
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = self.get_user(validated_token)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
"JWT: utilisateur introuvable pour %s %s — %s",
|
||||||
|
request.method, request.path, str(e)
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
logger.debug("JWT: authentification réussie user_id=%s pour %s %s",
|
||||||
|
user.pk, request.method, request.path)
|
||||||
|
return user, validated_token
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.1.3 on 2025-11-30 11:02
|
# Generated by Django 5.1.3 on 2026-03-14 13:23
|
||||||
|
|
||||||
import django.contrib.auth.models
|
import django.contrib.auth.models
|
||||||
import django.contrib.auth.validators
|
import django.contrib.auth.validators
|
||||||
@ -24,7 +24,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('role_type', models.IntegerField(choices=[(-1, 'NON DEFINI'), (0, 'ECOLE'), (1, 'ADMIN'), (2, 'PARENT')], default=-1)),
|
('role_type', models.IntegerField(choices=[(-1, 'NON DEFINI'), (0, 'ECOLE'), (1, 'ADMIN'), (2, 'PARENT')], default=-1)),
|
||||||
('is_active', models.BooleanField(default=False)),
|
('is_active', models.BooleanField(blank=True, default=False)),
|
||||||
('updated_date', models.DateTimeField(auto_now=True)),
|
('updated_date', models.DateTimeField(auto_now=True)),
|
||||||
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='profile_roles', to='Establishment.establishment')),
|
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='profile_roles', to='Establishment.establishment')),
|
||||||
],
|
],
|
||||||
|
|||||||
553
Back-End/Auth/tests.py
Normal file
553
Back-End/Auth/tests.py
Normal file
@ -0,0 +1,553 @@
|
|||||||
|
"""
|
||||||
|
Tests unitaires pour le module Auth.
|
||||||
|
Vérifie :
|
||||||
|
- L'accès public aux endpoints de login/CSRF/subscribe
|
||||||
|
- La protection JWT des endpoints protégés (profils, rôles, session)
|
||||||
|
- La génération et validation des tokens JWT
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from django.test import TestCase, override_settings
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
|
||||||
|
from Auth.models import Profile, ProfileRole
|
||||||
|
from Establishment.models import Establishment
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def create_establishment():
|
||||||
|
"""Crée un établissement minimal utilisé dans les tests."""
|
||||||
|
return Establishment.objects.create(
|
||||||
|
name="Ecole Test",
|
||||||
|
address="1 rue de l'Ecole",
|
||||||
|
total_capacity=100,
|
||||||
|
establishment_type=[1],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_user(email="test@example.com", password="testpassword123"):
|
||||||
|
"""Crée un utilisateur (Profile) de test."""
|
||||||
|
user = Profile.objects.create_user(
|
||||||
|
username=email,
|
||||||
|
email=email,
|
||||||
|
password=password,
|
||||||
|
)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def create_active_user_with_role(email="active@example.com", password="testpassword123"):
|
||||||
|
"""Crée un utilisateur avec un rôle actif."""
|
||||||
|
user = create_user(email=email, password=password)
|
||||||
|
establishment = create_establishment()
|
||||||
|
ProfileRole.objects.create(
|
||||||
|
profile=user,
|
||||||
|
role_type=ProfileRole.RoleType.PROFIL_ADMIN,
|
||||||
|
establishment=establishment,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def get_jwt_token(user):
|
||||||
|
"""Retourne un token d'accès JWT pour l'utilisateur donné."""
|
||||||
|
refresh = RefreshToken.for_user(user)
|
||||||
|
return str(refresh.access_token)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests endpoints publics
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
CACHES={'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}},
|
||||||
|
SESSION_ENGINE='django.contrib.sessions.backends.db',
|
||||||
|
)
|
||||||
|
class CsrfEndpointTest(TestCase):
|
||||||
|
"""Test de l'endpoint CSRF – doit être accessible sans authentification."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
|
||||||
|
def test_csrf_endpoint_accessible_sans_auth(self):
|
||||||
|
"""GET /Auth/csrf doit retourner 200 sans token."""
|
||||||
|
response = self.client.get(reverse("Auth:csrf"))
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertIn("csrfToken", response.json())
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
CACHES={'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}},
|
||||||
|
SESSION_ENGINE='django.contrib.sessions.backends.db',
|
||||||
|
)
|
||||||
|
class LoginEndpointTest(TestCase):
|
||||||
|
"""Tests de l'endpoint de connexion."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.url = reverse("Auth:login")
|
||||||
|
self.user = create_active_user_with_role(
|
||||||
|
email="logintest@example.com", password="secureP@ss1"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_login_avec_identifiants_valides(self):
|
||||||
|
"""POST /Auth/login avec identifiants valides retourne 200 et un token."""
|
||||||
|
payload = {"email": "logintest@example.com", "password": "secureP@ss1"}
|
||||||
|
response = self.client.post(
|
||||||
|
self.url, data=json.dumps(payload), content_type="application/json"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertIn("token", data)
|
||||||
|
self.assertIn("refresh", data)
|
||||||
|
|
||||||
|
def test_login_avec_mauvais_mot_de_passe(self):
|
||||||
|
"""POST /Auth/login avec mauvais mot de passe retourne 400 ou 401."""
|
||||||
|
payload = {"email": "logintest@example.com", "password": "wrongpassword"}
|
||||||
|
response = self.client.post(
|
||||||
|
self.url, data=json.dumps(payload), content_type="application/json"
|
||||||
|
)
|
||||||
|
self.assertIn(response.status_code, [status.HTTP_400_BAD_REQUEST, status.HTTP_401_UNAUTHORIZED])
|
||||||
|
|
||||||
|
def test_login_avec_email_inexistant(self):
|
||||||
|
"""POST /Auth/login avec email inconnu retourne 400 ou 401."""
|
||||||
|
payload = {"email": "unknown@example.com", "password": "anypassword"}
|
||||||
|
response = self.client.post(
|
||||||
|
self.url, data=json.dumps(payload), content_type="application/json"
|
||||||
|
)
|
||||||
|
self.assertIn(response.status_code, [status.HTTP_400_BAD_REQUEST, status.HTTP_401_UNAUTHORIZED])
|
||||||
|
|
||||||
|
def test_login_accessible_sans_authentification(self):
|
||||||
|
"""L'endpoint de login doit être accessible sans token JWT."""
|
||||||
|
# On vérifie juste que l'on n'obtient pas 401/403 pour raison d'auth manquante
|
||||||
|
payload = {"email": "logintest@example.com", "password": "secureP@ss1"}
|
||||||
|
response = self.client.post(
|
||||||
|
self.url, data=json.dumps(payload), content_type="application/json"
|
||||||
|
)
|
||||||
|
self.assertNotEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
self.assertNotEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
CACHES={'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}},
|
||||||
|
SESSION_ENGINE='django.contrib.sessions.backends.db',
|
||||||
|
)
|
||||||
|
class RefreshJWTEndpointTest(TestCase):
|
||||||
|
"""Tests de l'endpoint de rafraîchissement du token JWT."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.url = reverse("Auth:refresh_jwt")
|
||||||
|
self.user = create_active_user_with_role(email="refresh@example.com")
|
||||||
|
|
||||||
|
def test_refresh_avec_token_valide(self):
|
||||||
|
"""POST /Auth/refreshJWT avec refresh token valide retourne un nouvel access token."""
|
||||||
|
import jwt, uuid
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from django.conf import settings as django_settings
|
||||||
|
# RefreshJWTView attend le format custom (type='refresh'), pas le format SimpleJWT
|
||||||
|
refresh_payload = {
|
||||||
|
'user_id': self.user.id,
|
||||||
|
'type': 'refresh',
|
||||||
|
'jti': str(uuid.uuid4()),
|
||||||
|
'exp': datetime.utcnow() + timedelta(days=1),
|
||||||
|
'iat': datetime.utcnow(),
|
||||||
|
}
|
||||||
|
custom_refresh = jwt.encode(
|
||||||
|
refresh_payload,
|
||||||
|
django_settings.SIMPLE_JWT['SIGNING_KEY'],
|
||||||
|
algorithm=django_settings.SIMPLE_JWT['ALGORITHM'],
|
||||||
|
)
|
||||||
|
payload = {"refresh": custom_refresh}
|
||||||
|
response = self.client.post(
|
||||||
|
self.url, data=json.dumps(payload), content_type="application/json"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertIn("token", response.json())
|
||||||
|
|
||||||
|
def test_refresh_avec_token_invalide(self):
|
||||||
|
"""POST /Auth/refreshJWT avec token invalide retourne 401."""
|
||||||
|
payload = {"refresh": "invalid.token.here"}
|
||||||
|
response = self.client.post(
|
||||||
|
self.url, data=json.dumps(payload), content_type="application/json"
|
||||||
|
)
|
||||||
|
self.assertIn(response.status_code, [status.HTTP_400_BAD_REQUEST, status.HTTP_401_UNAUTHORIZED])
|
||||||
|
|
||||||
|
def test_refresh_accessible_sans_authentification(self):
|
||||||
|
"""L'endpoint de refresh doit être accessible sans token d'accès."""
|
||||||
|
refresh = RefreshToken.for_user(self.user)
|
||||||
|
payload = {"refresh": str(refresh)}
|
||||||
|
response = self.client.post(
|
||||||
|
self.url, data=json.dumps(payload), content_type="application/json"
|
||||||
|
)
|
||||||
|
self.assertNotEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests endpoints protégés – Session
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
CACHES={'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}},
|
||||||
|
SESSION_ENGINE='django.contrib.sessions.backends.db',
|
||||||
|
)
|
||||||
|
class SessionEndpointTest(TestCase):
|
||||||
|
"""Tests de l'endpoint d'information de session."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.url = reverse("Auth:infoSession")
|
||||||
|
self.user = create_active_user_with_role(email="session@example.com")
|
||||||
|
|
||||||
|
def test_info_session_sans_token_retourne_401(self):
|
||||||
|
"""GET /Auth/infoSession sans token doit retourner 401."""
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_info_session_avec_token_valide_retourne_200(self):
|
||||||
|
"""GET /Auth/infoSession avec token valide doit retourner 200 et les données utilisateur."""
|
||||||
|
token = get_jwt_token(self.user)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertIn("user", data)
|
||||||
|
self.assertEqual(data["user"]["email"], self.user.email)
|
||||||
|
|
||||||
|
def test_info_session_avec_token_invalide_retourne_401(self):
|
||||||
|
"""GET /Auth/infoSession avec token invalide doit retourner 401."""
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION="Bearer token.invalide.xyz")
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_info_session_avec_token_expire_retourne_401(self):
|
||||||
|
"""GET /Auth/infoSession avec un token expiré doit retourner 401."""
|
||||||
|
import jwt
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from django.conf import settings as django_settings
|
||||||
|
|
||||||
|
expired_payload = {
|
||||||
|
'user_id': self.user.id,
|
||||||
|
'exp': datetime.utcnow() - timedelta(hours=1),
|
||||||
|
}
|
||||||
|
expired_token = jwt.encode(
|
||||||
|
expired_payload, django_settings.SECRET_KEY, algorithm='HS256'
|
||||||
|
)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {expired_token}")
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests endpoints protégés – Profils
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
CACHES={'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}},
|
||||||
|
SESSION_ENGINE='django.contrib.sessions.backends.db',
|
||||||
|
REST_FRAMEWORK={
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
|
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||||
|
),
|
||||||
|
'DEFAULT_PERMISSION_CLASSES': (
|
||||||
|
'rest_framework.permissions.IsAuthenticated',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
class ProfileEndpointAuthTest(TestCase):
|
||||||
|
"""Tests d'authentification sur les endpoints de profils."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.profiles_url = reverse("Auth:profile")
|
||||||
|
self.user = create_active_user_with_role(email="profile_auth@example.com")
|
||||||
|
|
||||||
|
def test_get_profiles_sans_auth_retourne_401(self):
|
||||||
|
"""GET /Auth/profiles sans token doit retourner 401."""
|
||||||
|
response = self.client.get(self.profiles_url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_get_profiles_avec_auth_retourne_200(self):
|
||||||
|
"""GET /Auth/profiles avec token valide doit retourner 200."""
|
||||||
|
token = get_jwt_token(self.user)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||||
|
response = self.client.get(self.profiles_url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def test_post_profile_sans_auth_retourne_401(self):
|
||||||
|
"""POST /Auth/profiles sans token doit retourner 401."""
|
||||||
|
payload = {"email": "new@example.com", "password": "pass123"}
|
||||||
|
response = self.client.post(
|
||||||
|
self.profiles_url,
|
||||||
|
data=json.dumps(payload),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_get_profile_par_id_sans_auth_retourne_401(self):
|
||||||
|
"""GET /Auth/profiles/{id} sans token doit retourner 401."""
|
||||||
|
url = reverse("Auth:profile", kwargs={"id": self.user.id})
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_put_profile_sans_auth_retourne_401(self):
|
||||||
|
"""PUT /Auth/profiles/{id} sans token doit retourner 401."""
|
||||||
|
url = reverse("Auth:profile", kwargs={"id": self.user.id})
|
||||||
|
payload = {"email": self.user.email}
|
||||||
|
response = self.client.put(
|
||||||
|
url, data=json.dumps(payload), content_type="application/json"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_delete_profile_sans_auth_retourne_401(self):
|
||||||
|
"""DELETE /Auth/profiles/{id} sans token doit retourner 401."""
|
||||||
|
url = reverse("Auth:profile", kwargs={"id": self.user.id})
|
||||||
|
response = self.client.delete(url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests endpoints protégés – ProfileRole
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
CACHES={'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}},
|
||||||
|
SESSION_ENGINE='django.contrib.sessions.backends.db',
|
||||||
|
REST_FRAMEWORK={
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
|
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||||
|
),
|
||||||
|
'DEFAULT_PERMISSION_CLASSES': (
|
||||||
|
'rest_framework.permissions.IsAuthenticated',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
class ProfileRoleEndpointAuthTest(TestCase):
|
||||||
|
"""Tests d'authentification sur les endpoints de rôles."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.profile_roles_url = reverse("Auth:profileRoles")
|
||||||
|
self.user = create_active_user_with_role(email="roles_auth@example.com")
|
||||||
|
|
||||||
|
def test_get_profile_roles_sans_auth_retourne_401(self):
|
||||||
|
"""GET /Auth/profileRoles sans token doit retourner 401."""
|
||||||
|
response = self.client.get(self.profile_roles_url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_get_profile_roles_avec_auth_retourne_200(self):
|
||||||
|
"""GET /Auth/profileRoles avec token valide doit retourner 200."""
|
||||||
|
token = get_jwt_token(self.user)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||||
|
response = self.client.get(self.profile_roles_url)
|
||||||
|
self.assertNotIn(
|
||||||
|
response.status_code,
|
||||||
|
[status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN],
|
||||||
|
msg="Un token valide ne doit pas être rejeté par la couche d'authentification",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_post_profile_role_sans_auth_retourne_401(self):
|
||||||
|
"""POST /Auth/profileRoles sans token doit retourner 401."""
|
||||||
|
payload = {"profile": self.user.id, "role_type": 1}
|
||||||
|
response = self.client.post(
|
||||||
|
self.profile_roles_url,
|
||||||
|
data=json.dumps(payload),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests de génération de token JWT
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
CACHES={'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}},
|
||||||
|
SESSION_ENGINE='django.contrib.sessions.backends.db',
|
||||||
|
)
|
||||||
|
class JWTTokenGenerationTest(TestCase):
|
||||||
|
"""Tests de génération et validation des tokens JWT."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = create_user(email="jwt@example.com", password="jwttest123")
|
||||||
|
|
||||||
|
def test_generation_token_valide(self):
|
||||||
|
"""Un token généré pour un utilisateur est valide et contient user_id."""
|
||||||
|
import jwt
|
||||||
|
from django.conf import settings as django_settings
|
||||||
|
|
||||||
|
token = get_jwt_token(self.user)
|
||||||
|
self.assertIsNotNone(token)
|
||||||
|
self.assertIsInstance(token, str)
|
||||||
|
decoded = jwt.decode(token, django_settings.SECRET_KEY, algorithms=["HS256"])
|
||||||
|
self.assertEqual(decoded["user_id"], self.user.id)
|
||||||
|
|
||||||
|
def test_refresh_token_permet_obtenir_nouvel_access_token(self):
|
||||||
|
"""Le refresh token permet d'obtenir un nouvel access token via SimpleJWT."""
|
||||||
|
refresh = RefreshToken.for_user(self.user)
|
||||||
|
access = refresh.access_token
|
||||||
|
self.assertIsNotNone(str(access))
|
||||||
|
self.assertIsNotNone(str(refresh))
|
||||||
|
|
||||||
|
def test_token_different_par_utilisateur(self):
|
||||||
|
"""Deux utilisateurs différents ont des tokens différents."""
|
||||||
|
user2 = create_user(email="jwt2@example.com", password="jwttest123")
|
||||||
|
token1 = get_jwt_token(self.user)
|
||||||
|
token2 = get_jwt_token(user2)
|
||||||
|
self.assertNotEqual(token1, token2)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests de sécurité — Correction des vulnérabilités identifiées
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
CACHES={'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}},
|
||||||
|
SESSION_ENGINE='django.contrib.sessions.backends.db',
|
||||||
|
)
|
||||||
|
class SessionViewTokenTypeTest(TestCase):
|
||||||
|
"""
|
||||||
|
SessionView doit rejeter les refresh tokens.
|
||||||
|
Avant la correction, jwt.decode() était appelé sans vérification du claim 'type',
|
||||||
|
ce qui permettait d'utiliser un refresh token là où seul un access token est attendu.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.url = reverse("Auth:infoSession")
|
||||||
|
self.user = create_active_user_with_role(email="session_type@example.com")
|
||||||
|
|
||||||
|
def test_refresh_token_rejete_par_session_view(self):
|
||||||
|
"""
|
||||||
|
Utiliser un refresh token SimpleJWT sur /infoSession doit retourner 401.
|
||||||
|
"""
|
||||||
|
import jwt
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from django.conf import settings as django_settings
|
||||||
|
|
||||||
|
# Fabriquer manuellement un token de type 'refresh' signé avec la clé correcte
|
||||||
|
refresh_payload = {
|
||||||
|
'user_id': self.user.id,
|
||||||
|
'type': 'refresh', # ← type incorrect pour cet endpoint
|
||||||
|
'jti': 'test-refresh-jti',
|
||||||
|
'exp': datetime.utcnow() + timedelta(days=1),
|
||||||
|
'iat': datetime.utcnow(),
|
||||||
|
}
|
||||||
|
refresh_token = jwt.encode(
|
||||||
|
refresh_payload, django_settings.SECRET_KEY, algorithm='HS256'
|
||||||
|
)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {refresh_token}")
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertEqual(
|
||||||
|
response.status_code, status.HTTP_401_UNAUTHORIZED,
|
||||||
|
"Un refresh token ne doit pas être accepté sur /infoSession (OWASP A07 - Auth Failures)"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_access_token_accepte_par_session_view(self):
|
||||||
|
"""Un access token de type 'access' est accepté."""
|
||||||
|
import jwt
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from django.conf import settings as django_settings
|
||||||
|
|
||||||
|
access_payload = {
|
||||||
|
'user_id': self.user.id,
|
||||||
|
'type': 'access',
|
||||||
|
'jti': 'test-access-jti',
|
||||||
|
'exp': datetime.utcnow() + timedelta(minutes=15),
|
||||||
|
'iat': datetime.utcnow(),
|
||||||
|
}
|
||||||
|
access_token = jwt.encode(
|
||||||
|
access_payload, django_settings.SECRET_KEY, algorithm='HS256'
|
||||||
|
)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {access_token}")
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
CACHES={'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}},
|
||||||
|
SESSION_ENGINE='django.contrib.sessions.backends.db',
|
||||||
|
)
|
||||||
|
class RefreshJWTErrorLeakTest(TestCase):
|
||||||
|
"""
|
||||||
|
RefreshJWTView ne doit pas retourner les messages d'exception internes.
|
||||||
|
Avant la correction, str(e) était renvoyé directement au client.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.url = reverse("Auth:refresh_jwt")
|
||||||
|
|
||||||
|
def test_token_invalide_ne_revele_pas_details_internes(self):
|
||||||
|
"""
|
||||||
|
Un token invalide doit retourner un message générique, pas les détails de l'exception.
|
||||||
|
"""
|
||||||
|
payload = {"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.forged.signature"}
|
||||||
|
response = self.client.post(
|
||||||
|
self.url, data=json.dumps(payload), content_type="application/json"
|
||||||
|
)
|
||||||
|
self.assertIn(response.status_code, [status.HTTP_400_BAD_REQUEST, status.HTTP_401_UNAUTHORIZED])
|
||||||
|
body = response.content.decode()
|
||||||
|
# Le message ne doit pas contenir de traceback ou de détails internes de bibliothèque
|
||||||
|
self.assertNotIn("Traceback", body)
|
||||||
|
self.assertNotIn("jwt.exceptions", body)
|
||||||
|
self.assertNotIn("simplejwt", body.lower())
|
||||||
|
|
||||||
|
def test_erreur_reponse_est_generique(self):
|
||||||
|
"""
|
||||||
|
Le message d'erreur doit être 'Token invalide' (générique), pas le str(e).
|
||||||
|
"""
|
||||||
|
payload = {"refresh": "bad.token.data"}
|
||||||
|
response = self.client.post(
|
||||||
|
self.url, data=json.dumps(payload), content_type="application/json"
|
||||||
|
)
|
||||||
|
data = response.json()
|
||||||
|
self.assertIn('errorMessage', data)
|
||||||
|
# Le message doit être le message générique, pas la chaîne brute de l'exception
|
||||||
|
self.assertIn(data['errorMessage'], ['Token invalide', 'Format de token invalide',
|
||||||
|
'Refresh token expiré', 'Erreur interne du serveur'])
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
CACHES={'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}},
|
||||||
|
SESSION_ENGINE='django.contrib.sessions.backends.db',
|
||||||
|
)
|
||||||
|
class SecurityHeadersTest(TestCase):
|
||||||
|
"""
|
||||||
|
Les en-têtes de sécurité HTTP doivent être présents dans toutes les réponses.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
|
||||||
|
def test_x_content_type_options_present(self):
|
||||||
|
"""X-Content-Type-Options: nosniff doit être présent."""
|
||||||
|
response = self.client.get(reverse("Auth:csrf"))
|
||||||
|
self.assertEqual(
|
||||||
|
response.get('X-Content-Type-Options'), 'nosniff',
|
||||||
|
"X-Content-Type-Options: nosniff doit être défini (prévient le MIME sniffing)"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_referrer_policy_present(self):
|
||||||
|
"""Referrer-Policy doit être présent."""
|
||||||
|
response = self.client.get(reverse("Auth:csrf"))
|
||||||
|
self.assertIsNotNone(
|
||||||
|
response.get('Referrer-Policy'),
|
||||||
|
"Referrer-Policy doit être défini"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_csp_frame_ancestors_present(self):
|
||||||
|
"""Content-Security-Policy doit contenir frame-ancestors."""
|
||||||
|
response = self.client.get(reverse("Auth:csrf"))
|
||||||
|
csp = response.get('Content-Security-Policy', '')
|
||||||
|
self.assertIn('frame-ancestors', csp,
|
||||||
|
"CSP doit définir frame-ancestors (protection clickjacking)")
|
||||||
|
self.assertIn("object-src 'none'", csp,
|
||||||
|
"CSP doit définir object-src 'none' (prévient les plugins malveillants)")
|
||||||
|
|
||||||
@ -17,10 +17,12 @@ from datetime import datetime, timedelta
|
|||||||
import jwt
|
import jwt
|
||||||
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
|
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
|
||||||
import json
|
import json
|
||||||
|
import uuid
|
||||||
|
|
||||||
from . import validator
|
from . import validator
|
||||||
from .models import Profile, ProfileRole
|
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 django.db.models import Q
|
||||||
|
|
||||||
from Auth.serializers import ProfileSerializer, ProfileRoleSerializer
|
from Auth.serializers import ProfileSerializer, ProfileRoleSerializer
|
||||||
@ -28,13 +30,28 @@ from Subscriptions.models import RegistrationForm, Guardian
|
|||||||
import N3wtSchool.mailManager as mailer
|
import N3wtSchool.mailManager as mailer
|
||||||
import Subscriptions.util as util
|
import Subscriptions.util as util
|
||||||
import logging
|
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
|
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||||
|
|
||||||
logger = logging.getLogger("AuthViews")
|
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(
|
@swagger_auto_schema(
|
||||||
method='get',
|
method='get',
|
||||||
operation_description="Obtenir un token CSRF",
|
operation_description="Obtenir un token CSRF",
|
||||||
@ -43,11 +60,15 @@ logger = logging.getLogger("AuthViews")
|
|||||||
}))}
|
}))}
|
||||||
)
|
)
|
||||||
@api_view(['GET'])
|
@api_view(['GET'])
|
||||||
|
@permission_classes([AllowAny])
|
||||||
def csrf(request):
|
def csrf(request):
|
||||||
token = get_token(request)
|
token = get_token(request)
|
||||||
return JsonResponse({'csrfToken': token})
|
return JsonResponse({'csrfToken': token})
|
||||||
|
|
||||||
class SessionView(APIView):
|
class SessionView(APIView):
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
authentication_classes = [] # SessionView gère sa propre validation JWT
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
operation_description="Vérifier une session utilisateur",
|
operation_description="Vérifier une session utilisateur",
|
||||||
manual_parameters=[openapi.Parameter('Authorization', openapi.IN_HEADER, type=openapi.TYPE_STRING, description='Bearer token')],
|
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]
|
token = request.META.get('HTTP_AUTHORIZATION', '').split('Bearer ')[-1]
|
||||||
try:
|
try:
|
||||||
decoded_token = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256'])
|
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')
|
userid = decoded_token.get('user_id')
|
||||||
user = Profile.objects.get(id=userid)
|
user = Profile.objects.get(id=userid)
|
||||||
roles = ProfileRole.objects.filter(profile=user).values('role_type', 'establishment__name')
|
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)
|
return JsonResponse({"error": "Invalid token"}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
class ProfileView(APIView):
|
class ProfileView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
operation_description="Obtenir la liste des profils",
|
operation_description="Obtenir la liste des profils",
|
||||||
responses={200: ProfileSerializer(many=True)}
|
responses={200: ProfileSerializer(many=True)}
|
||||||
@ -118,6 +146,8 @@ class ProfileView(APIView):
|
|||||||
@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')
|
||||||
class ProfileSimpleView(APIView):
|
class ProfileSimpleView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
operation_description="Obtenir un profil par son ID",
|
operation_description="Obtenir un profil par son ID",
|
||||||
responses={200: ProfileSerializer}
|
responses={200: ProfileSerializer}
|
||||||
@ -152,8 +182,12 @@ 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_exempt, name='dispatch')
|
@method_decorator(csrf_protect, name='dispatch')
|
||||||
|
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||||
class LoginView(APIView):
|
class LoginView(APIView):
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
throttle_classes = [LoginRateThrottle]
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
operation_description="Connexion utilisateur",
|
operation_description="Connexion utilisateur",
|
||||||
request_body=openapi.Schema(
|
request_body=openapi.Schema(
|
||||||
@ -240,12 +274,14 @@ def makeToken(user):
|
|||||||
})
|
})
|
||||||
|
|
||||||
# Générer le JWT avec la bonne syntaxe datetime
|
# 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 = {
|
access_payload = {
|
||||||
'user_id': user.id,
|
'user_id': user.id,
|
||||||
'email': user.email,
|
'email': user.email,
|
||||||
'roleIndexLoginDefault': user.roleIndexLoginDefault,
|
'roleIndexLoginDefault': user.roleIndexLoginDefault,
|
||||||
'roles': roles,
|
'roles': roles,
|
||||||
'type': 'access',
|
'type': 'access',
|
||||||
|
'jti': str(uuid.uuid4()),
|
||||||
'exp': datetime.utcnow() + settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'],
|
'exp': datetime.utcnow() + settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'],
|
||||||
'iat': datetime.utcnow(),
|
'iat': datetime.utcnow(),
|
||||||
}
|
}
|
||||||
@ -255,16 +291,23 @@ def makeToken(user):
|
|||||||
refresh_payload = {
|
refresh_payload = {
|
||||||
'user_id': user.id,
|
'user_id': user.id,
|
||||||
'type': 'refresh',
|
'type': 'refresh',
|
||||||
|
'jti': str(uuid.uuid4()),
|
||||||
'exp': datetime.utcnow() + settings.SIMPLE_JWT['REFRESH_TOKEN_LIFETIME'],
|
'exp': datetime.utcnow() + settings.SIMPLE_JWT['REFRESH_TOKEN_LIFETIME'],
|
||||||
'iat': datetime.utcnow(),
|
'iat': datetime.utcnow(),
|
||||||
}
|
}
|
||||||
refresh_token = jwt.encode(refresh_payload, settings.SIMPLE_JWT['SIGNING_KEY'], algorithm=settings.SIMPLE_JWT['ALGORITHM'])
|
refresh_token = jwt.encode(refresh_payload, settings.SIMPLE_JWT['SIGNING_KEY'], algorithm=settings.SIMPLE_JWT['ALGORITHM'])
|
||||||
return access_token, refresh_token
|
return access_token, refresh_token
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Erreur lors de la création du token: {str(e)}")
|
logger.error(f"Erreur lors de la création du token: {str(e)}", exc_info=True)
|
||||||
return None
|
# 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):
|
class RefreshJWTView(APIView):
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
throttle_classes = [LoginRateThrottle]
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
operation_description="Rafraîchir le token d'accès",
|
operation_description="Rafraîchir le token d'accès",
|
||||||
request_body=openapi.Schema(
|
request_body=openapi.Schema(
|
||||||
@ -290,7 +333,6 @@ class RefreshJWTView(APIView):
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@method_decorator(csrf_exempt, name='dispatch')
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
data = JSONParser().parse(request)
|
data = JSONParser().parse(request)
|
||||||
refresh_token = data.get("refresh")
|
refresh_token = data.get("refresh")
|
||||||
@ -335,14 +377,16 @@ class RefreshJWTView(APIView):
|
|||||||
return JsonResponse({'errorMessage': 'Refresh token expiré'}, status=400)
|
return JsonResponse({'errorMessage': 'Refresh token expiré'}, status=400)
|
||||||
except InvalidTokenError as e:
|
except InvalidTokenError as e:
|
||||||
logger.error(f"Token invalide: {str(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:
|
except Exception as e:
|
||||||
logger.error(f"Erreur inattendue: {str(e)}")
|
logger.error(f"Erreur inattendue: {str(e)}", exc_info=True)
|
||||||
return JsonResponse({'errorMessage': f'Erreur inattendue: {str(e)}'}, status=400)
|
return JsonResponse({'errorMessage': 'Erreur interne du serveur'}, status=500)
|
||||||
|
|
||||||
@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')
|
||||||
class SubscribeView(APIView):
|
class SubscribeView(APIView):
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
operation_description="Inscription utilisateur",
|
operation_description="Inscription utilisateur",
|
||||||
manual_parameters=[
|
manual_parameters=[
|
||||||
@ -430,6 +474,8 @@ class SubscribeView(APIView):
|
|||||||
@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')
|
||||||
class NewPasswordView(APIView):
|
class NewPasswordView(APIView):
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
operation_description="Demande de nouveau mot de passe",
|
operation_description="Demande de nouveau mot de passe",
|
||||||
request_body=openapi.Schema(
|
request_body=openapi.Schema(
|
||||||
@ -479,6 +525,8 @@ class NewPasswordView(APIView):
|
|||||||
@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')
|
||||||
class ResetPasswordView(APIView):
|
class ResetPasswordView(APIView):
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
operation_description="Réinitialisation du mot de passe",
|
operation_description="Réinitialisation du mot de passe",
|
||||||
request_body=openapi.Schema(
|
request_body=openapi.Schema(
|
||||||
@ -525,7 +573,9 @@ class ResetPasswordView(APIView):
|
|||||||
return JsonResponse({'message': retour, "errorMessage": retourErreur, "errorFields": errorFields}, safe=False)
|
return JsonResponse({'message': retour, "errorMessage": retourErreur, "errorFields": errorFields}, safe=False)
|
||||||
|
|
||||||
class ProfileRoleView(APIView):
|
class ProfileRoleView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
pagination_class = CustomProfilesPagination
|
pagination_class = CustomProfilesPagination
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
operation_description="Obtenir la liste des profile_roles",
|
operation_description="Obtenir la liste des profile_roles",
|
||||||
responses={200: ProfileRoleSerializer(many=True)}
|
responses={200: ProfileRoleSerializer(many=True)}
|
||||||
@ -596,6 +646,8 @@ class ProfileRoleView(APIView):
|
|||||||
@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')
|
||||||
class ProfileRoleSimpleView(APIView):
|
class ProfileRoleSimpleView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
operation_description="Obtenir un profile_role par son ID",
|
operation_description="Obtenir un profile_role par son ID",
|
||||||
responses={200: ProfileRoleSerializer}
|
responses={200: ProfileRoleSerializer}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.1.3 on 2025-11-30 11:02
|
# Generated by Django 5.1.3 on 2026-03-14 13:23
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|||||||
@ -1,3 +1,145 @@
|
|||||||
from django.test import TestCase
|
"""
|
||||||
|
Tests unitaires pour le module Common.
|
||||||
|
Vérifie que les endpoints Domain et Category requièrent une authentification JWT.
|
||||||
|
"""
|
||||||
|
|
||||||
# Create your tests here.
|
import json
|
||||||
|
|
||||||
|
from django.test import TestCase, override_settings
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
|
||||||
|
from Auth.models import Profile
|
||||||
|
|
||||||
|
|
||||||
|
def create_user(email="common_test@example.com", password="testpassword123"):
|
||||||
|
return Profile.objects.create_user(username=email, email=email, password=password)
|
||||||
|
|
||||||
|
|
||||||
|
def get_jwt_token(user):
|
||||||
|
refresh = RefreshToken.for_user(user)
|
||||||
|
return str(refresh.access_token)
|
||||||
|
|
||||||
|
|
||||||
|
TEST_REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
|
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||||
|
),
|
||||||
|
'DEFAULT_PERMISSION_CLASSES': (
|
||||||
|
'rest_framework.permissions.IsAuthenticated',
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CACHES = {'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Domain
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
CACHES=TEST_CACHES,
|
||||||
|
SESSION_ENGINE='django.contrib.sessions.backends.db',
|
||||||
|
REST_FRAMEWORK=TEST_REST_FRAMEWORK,
|
||||||
|
)
|
||||||
|
class DomainEndpointAuthTest(TestCase):
|
||||||
|
"""Tests d'authentification sur les endpoints Domain."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.list_url = reverse("Common:domain_list_create")
|
||||||
|
self.user = create_user()
|
||||||
|
|
||||||
|
def test_get_domains_sans_auth_retourne_401(self):
|
||||||
|
"""GET /Common/domains sans token doit retourner 401."""
|
||||||
|
response = self.client.get(self.list_url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_post_domain_sans_auth_retourne_401(self):
|
||||||
|
"""POST /Common/domains sans token doit retourner 401."""
|
||||||
|
response = self.client.post(
|
||||||
|
self.list_url,
|
||||||
|
data=json.dumps({"name": "Musique"}),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_get_domains_avec_auth_retourne_200(self):
|
||||||
|
"""GET /Common/domains avec token valide doit retourner 200."""
|
||||||
|
token = get_jwt_token(self.user)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||||
|
response = self.client.get(self.list_url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def test_put_domain_sans_auth_retourne_401(self):
|
||||||
|
"""PUT /Common/domains/{id} sans token doit retourner 401."""
|
||||||
|
url = reverse("Common:domain_detail", kwargs={"id": 1})
|
||||||
|
response = self.client.put(
|
||||||
|
url,
|
||||||
|
data=json.dumps({"name": "Danse"}),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_delete_domain_sans_auth_retourne_401(self):
|
||||||
|
"""DELETE /Common/domains/{id} sans token doit retourner 401."""
|
||||||
|
url = reverse("Common:domain_detail", kwargs={"id": 1})
|
||||||
|
response = self.client.delete(url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Category
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
CACHES=TEST_CACHES,
|
||||||
|
SESSION_ENGINE='django.contrib.sessions.backends.db',
|
||||||
|
REST_FRAMEWORK=TEST_REST_FRAMEWORK,
|
||||||
|
)
|
||||||
|
class CategoryEndpointAuthTest(TestCase):
|
||||||
|
"""Tests d'authentification sur les endpoints Category."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.list_url = reverse("Common:category_list_create")
|
||||||
|
self.user = create_user(email="category_test@example.com")
|
||||||
|
|
||||||
|
def test_get_categories_sans_auth_retourne_401(self):
|
||||||
|
"""GET /Common/categories sans token doit retourner 401."""
|
||||||
|
response = self.client.get(self.list_url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_post_category_sans_auth_retourne_401(self):
|
||||||
|
"""POST /Common/categories sans token doit retourner 401."""
|
||||||
|
response = self.client.post(
|
||||||
|
self.list_url,
|
||||||
|
data=json.dumps({"name": "Jazz"}),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_get_categories_avec_auth_retourne_200(self):
|
||||||
|
"""GET /Common/categories avec token valide doit retourner 200."""
|
||||||
|
token = get_jwt_token(self.user)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||||
|
response = self.client.get(self.list_url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def test_put_category_sans_auth_retourne_401(self):
|
||||||
|
"""PUT /Common/categories/{id} sans token doit retourner 401."""
|
||||||
|
url = reverse("Common:category_detail", kwargs={"id": 1})
|
||||||
|
response = self.client.put(
|
||||||
|
url,
|
||||||
|
data=json.dumps({"name": "Classique"}),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_delete_category_sans_auth_retourne_401(self):
|
||||||
|
"""DELETE /Common/categories/{id} sans token doit retourner 401."""
|
||||||
|
url = reverse("Common:category_detail", kwargs={"id": 1})
|
||||||
|
response = self.client.delete(url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|||||||
@ -4,6 +4,7 @@ from django.utils.decorators import method_decorator
|
|||||||
from rest_framework.parsers import JSONParser
|
from rest_framework.parsers import JSONParser
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from .models import (
|
from .models import (
|
||||||
Domain,
|
Domain,
|
||||||
Category
|
Category
|
||||||
@ -16,6 +17,8 @@ from .serializers import (
|
|||||||
@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')
|
||||||
class DomainListCreateView(APIView):
|
class DomainListCreateView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
domains = Domain.objects.all()
|
domains = Domain.objects.all()
|
||||||
serializer = DomainSerializer(domains, many=True)
|
serializer = DomainSerializer(domains, many=True)
|
||||||
@ -32,6 +35,8 @@ class DomainListCreateView(APIView):
|
|||||||
@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')
|
||||||
class DomainDetailView(APIView):
|
class DomainDetailView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request, id):
|
def get(self, request, id):
|
||||||
try:
|
try:
|
||||||
domain = Domain.objects.get(id=id)
|
domain = Domain.objects.get(id=id)
|
||||||
@ -65,6 +70,8 @@ class DomainDetailView(APIView):
|
|||||||
@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')
|
||||||
class CategoryListCreateView(APIView):
|
class CategoryListCreateView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
categories = Category.objects.all()
|
categories = Category.objects.all()
|
||||||
serializer = CategorySerializer(categories, many=True)
|
serializer = CategorySerializer(categories, many=True)
|
||||||
@ -81,6 +88,8 @@ class CategoryListCreateView(APIView):
|
|||||||
@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')
|
||||||
class CategoryDetailView(APIView):
|
class CategoryDetailView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request, id):
|
def get(self, request, id):
|
||||||
try:
|
try:
|
||||||
category = Category.objects.get(id=id)
|
category = Category.objects.get(id=id)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.1.3 on 2025-11-30 11:02
|
# Generated by Django 5.1.3 on 2026-03-14 13:23
|
||||||
|
|
||||||
import Establishment.models
|
import Establishment.models
|
||||||
import django.contrib.postgres.fields
|
import django.contrib.postgres.fields
|
||||||
|
|||||||
92
Back-End/Establishment/tests.py
Normal file
92
Back-End/Establishment/tests.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
"""
|
||||||
|
Tests unitaires pour le module Establishment.
|
||||||
|
Vérifie que les endpoints requièrent une authentification JWT.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.test import TestCase, override_settings
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
|
||||||
|
from Auth.models import Profile
|
||||||
|
|
||||||
|
|
||||||
|
def create_user(email="establishment_test@example.com", password="testpassword123"):
|
||||||
|
return Profile.objects.create_user(username=email, email=email, password=password)
|
||||||
|
|
||||||
|
|
||||||
|
def get_jwt_token(user):
|
||||||
|
refresh = RefreshToken.for_user(user)
|
||||||
|
return str(refresh.access_token)
|
||||||
|
|
||||||
|
|
||||||
|
TEST_REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
|
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||||
|
),
|
||||||
|
'DEFAULT_PERMISSION_CLASSES': (
|
||||||
|
'rest_framework.permissions.IsAuthenticated',
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CACHES = {'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}}
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
CACHES=TEST_CACHES,
|
||||||
|
SESSION_ENGINE='django.contrib.sessions.backends.db',
|
||||||
|
REST_FRAMEWORK=TEST_REST_FRAMEWORK,
|
||||||
|
)
|
||||||
|
class EstablishmentEndpointAuthTest(TestCase):
|
||||||
|
"""Tests d'authentification sur les endpoints Establishment."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.list_url = reverse("Establishment:establishment_list_create")
|
||||||
|
self.user = create_user()
|
||||||
|
|
||||||
|
def test_get_establishments_sans_auth_retourne_401(self):
|
||||||
|
"""GET /Establishment/establishments sans token doit retourner 401."""
|
||||||
|
response = self.client.get(self.list_url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_post_establishment_sans_auth_retourne_401(self):
|
||||||
|
"""POST /Establishment/establishments sans token doit retourner 401."""
|
||||||
|
import json
|
||||||
|
response = self.client.post(
|
||||||
|
self.list_url,
|
||||||
|
data=json.dumps({"name": "Ecole Alpha"}),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_get_establishment_detail_sans_auth_retourne_401(self):
|
||||||
|
"""GET /Establishment/establishments/{id} sans token doit retourner 401."""
|
||||||
|
url = reverse("Establishment:establishment_detail", kwargs={"id": 1})
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_put_establishment_sans_auth_retourne_401(self):
|
||||||
|
"""PUT /Establishment/establishments/{id} sans token doit retourner 401."""
|
||||||
|
import json
|
||||||
|
url = reverse("Establishment:establishment_detail", kwargs={"id": 1})
|
||||||
|
response = self.client.put(
|
||||||
|
url,
|
||||||
|
data=json.dumps({"name": "Ecole Beta"}),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_delete_establishment_sans_auth_retourne_401(self):
|
||||||
|
"""DELETE /Establishment/establishments/{id} sans token doit retourner 401."""
|
||||||
|
url = reverse("Establishment:establishment_detail", kwargs={"id": 1})
|
||||||
|
response = self.client.delete(url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_get_establishments_avec_auth_retourne_200(self):
|
||||||
|
"""GET /Establishment/establishments avec token valide doit retourner 200."""
|
||||||
|
token = get_jwt_token(self.user)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||||
|
response = self.client.get(self.list_url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
@ -4,6 +4,7 @@ from django.utils.decorators import method_decorator
|
|||||||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
from rest_framework.permissions import IsAuthenticated, BasePermission
|
||||||
from .models import Establishment
|
from .models import Establishment
|
||||||
from .serializers import EstablishmentSerializer
|
from .serializers import EstablishmentSerializer
|
||||||
from N3wtSchool.bdd import delete_object, getAllObjects, getObject
|
from N3wtSchool.bdd import delete_object, getAllObjects, getObject
|
||||||
@ -15,9 +16,29 @@ import N3wtSchool.mailManager as mailer
|
|||||||
import os
|
import os
|
||||||
from N3wtSchool import settings
|
from N3wtSchool import settings
|
||||||
|
|
||||||
@method_decorator(csrf_protect, name='dispatch')
|
|
||||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
class IsWebhookApiKey(BasePermission):
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
api_key = settings.WEBHOOK_API_KEY
|
||||||
|
if not api_key:
|
||||||
|
return False
|
||||||
|
return request.headers.get('X-API-Key') == api_key
|
||||||
|
|
||||||
|
|
||||||
|
class IsAuthenticatedOrWebhookApiKey(BasePermission):
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
if request.user and request.user.is_authenticated:
|
||||||
|
return True
|
||||||
|
return IsWebhookApiKey().has_permission(request, view)
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentListCreateView(APIView):
|
class EstablishmentListCreateView(APIView):
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
if self.request.method == 'POST':
|
||||||
|
return [IsAuthenticatedOrWebhookApiKey()]
|
||||||
|
return [IsAuthenticated()]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
establishments = getAllObjects(Establishment)
|
establishments = getAllObjects(Establishment)
|
||||||
establishments_serializer = EstablishmentSerializer(establishments, many=True)
|
establishments_serializer = EstablishmentSerializer(establishments, many=True)
|
||||||
@ -44,6 +65,7 @@ class EstablishmentListCreateView(APIView):
|
|||||||
@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')
|
||||||
class EstablishmentDetailView(APIView):
|
class EstablishmentDetailView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
parser_classes = [MultiPartParser, FormParser]
|
parser_classes = [MultiPartParser, FormParser]
|
||||||
|
|
||||||
def get(self, request, id=None):
|
def get(self, request, id=None):
|
||||||
@ -87,7 +109,9 @@ def create_establishment_with_directeur(establishment_data):
|
|||||||
directeur_email = directeur_data.get("email")
|
directeur_email = directeur_data.get("email")
|
||||||
last_name = directeur_data.get("last_name", "")
|
last_name = directeur_data.get("last_name", "")
|
||||||
first_name = directeur_data.get("first_name", "")
|
first_name = directeur_data.get("first_name", "")
|
||||||
password = directeur_data.get("password", "Provisoire01!")
|
password = directeur_data.get("password")
|
||||||
|
if not password:
|
||||||
|
raise ValueError("Le champ 'directeur.password' est obligatoire pour créer un établissement.")
|
||||||
|
|
||||||
# Création ou récupération du profil utilisateur
|
# Création ou récupération du profil utilisateur
|
||||||
profile, created = Profile.objects.get_or_create(
|
profile, created = Profile.objects.get_or_create(
|
||||||
|
|||||||
116
Back-End/GestionEmail/tests_security.py
Normal file
116
Back-End/GestionEmail/tests_security.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
"""
|
||||||
|
Tests de sécurité — GestionEmail
|
||||||
|
Vérifie :
|
||||||
|
- search_recipients nécessite une authentification (plus accessible anonymement)
|
||||||
|
- send-email nécessite une authentification
|
||||||
|
- Les données personnelles ne sont pas dans les logs INFO
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from django.test import TestCase, override_settings
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
|
||||||
|
from Auth.models import Profile, ProfileRole
|
||||||
|
from Establishment.models import Establishment
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def create_user_with_role(email, password="TestPass!123"):
|
||||||
|
user = Profile.objects.create_user(
|
||||||
|
username=email, email=email, password=password
|
||||||
|
)
|
||||||
|
est = Establishment.objects.create(
|
||||||
|
name=f"Ecole {email}",
|
||||||
|
address="1 rue Test",
|
||||||
|
total_capacity=50,
|
||||||
|
establishment_type=[1],
|
||||||
|
)
|
||||||
|
ProfileRole.objects.create(
|
||||||
|
profile=user,
|
||||||
|
role_type=ProfileRole.RoleType.PROFIL_ECOLE,
|
||||||
|
establishment=est,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
return user, est
|
||||||
|
|
||||||
|
|
||||||
|
OVERRIDE = dict(
|
||||||
|
CACHES={'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}},
|
||||||
|
SESSION_ENGINE='django.contrib.sessions.backends.db',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests : search_recipients exige une authentification
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(**OVERRIDE)
|
||||||
|
class SearchRecipientsAuthTest(TestCase):
|
||||||
|
"""
|
||||||
|
GET /email/search-recipients/ doit retourner 401 si non authentifié.
|
||||||
|
Avant la correction, cet endpoint était accessible anonymement
|
||||||
|
(harvesting d'emails des membres d'un établissement).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.url = reverse('GestionEmail:search_recipients')
|
||||||
|
|
||||||
|
def test_sans_auth_retourne_401(self):
|
||||||
|
"""Accès anonyme doit être rejeté avec 401."""
|
||||||
|
response = self.client.get(self.url, {'q': 'test', 'establishment_id': 1})
|
||||||
|
self.assertEqual(
|
||||||
|
response.status_code, status.HTTP_401_UNAUTHORIZED,
|
||||||
|
"search_recipients doit exiger une authentification (OWASP A01 - Broken Access Control)"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_avec_auth_et_query_vide_retourne_200_ou_liste_vide(self):
|
||||||
|
"""Un utilisateur authentifié sans terme de recherche reçoit une liste vide."""
|
||||||
|
user, est = create_user_with_role('search_auth@test.com')
|
||||||
|
token = str(RefreshToken.for_user(user).access_token)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')
|
||||||
|
response = self.client.get(self.url, {'q': '', 'establishment_id': est.id})
|
||||||
|
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_400_BAD_REQUEST])
|
||||||
|
|
||||||
|
def test_avec_auth_et_establishment_manquant_retourne_400(self):
|
||||||
|
"""Un utilisateur authentifié sans establishment_id reçoit 400."""
|
||||||
|
user, _ = create_user_with_role('search_noest@test.com')
|
||||||
|
token = str(RefreshToken.for_user(user).access_token)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')
|
||||||
|
response = self.client.get(self.url, {'q': 'alice'})
|
||||||
|
self.assertIn(response.status_code, [status.HTTP_400_BAD_REQUEST, status.HTTP_200_OK])
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests : send-email exige une authentification
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(**OVERRIDE)
|
||||||
|
class SendEmailAuthTest(TestCase):
|
||||||
|
"""
|
||||||
|
POST /email/send-email/ doit retourner 401 si non authentifié.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.url = reverse('GestionEmail:send_email')
|
||||||
|
|
||||||
|
def test_sans_auth_retourne_401(self):
|
||||||
|
"""Accès anonyme à l'envoi d'email doit être rejeté."""
|
||||||
|
payload = {
|
||||||
|
'recipients': ['victim@example.com'],
|
||||||
|
'subject': 'Test',
|
||||||
|
'message': 'Hello',
|
||||||
|
'establishment_id': 1,
|
||||||
|
}
|
||||||
|
response = self.client.post(
|
||||||
|
self.url, data=json.dumps(payload), content_type='application/json'
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
@ -2,6 +2,8 @@ from django.http.response import JsonResponse
|
|||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
from rest_framework.decorators import api_view, permission_classes
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from Auth.models import Profile, ProfileRole
|
from Auth.models import Profile, ProfileRole
|
||||||
|
|
||||||
@ -20,9 +22,11 @@ class SendEmailView(APIView):
|
|||||||
"""
|
"""
|
||||||
API pour envoyer des emails aux parents et professeurs.
|
API pour envoyer des emails aux parents et professeurs.
|
||||||
"""
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
# Ajouter du debug
|
# Ajouter du debug
|
||||||
logger.info(f"Request data received: {request.data}")
|
logger.info(f"Request data received (keys): {list(request.data.keys()) if request.data else []}") # Ne pas logger les valeurs (RGPD)
|
||||||
logger.info(f"Request content type: {request.content_type}")
|
logger.info(f"Request content type: {request.content_type}")
|
||||||
|
|
||||||
data = request.data
|
data = request.data
|
||||||
@ -34,11 +38,9 @@ class SendEmailView(APIView):
|
|||||||
establishment_id = data.get('establishment_id', '')
|
establishment_id = data.get('establishment_id', '')
|
||||||
|
|
||||||
# Debug des données reçues
|
# Debug des données reçues
|
||||||
logger.info(f"Recipients: {recipients} (type: {type(recipients)})")
|
logger.info(f"Recipients (count): {len(recipients)}")
|
||||||
logger.info(f"CC: {cc} (type: {type(cc)})")
|
|
||||||
logger.info(f"BCC: {bcc} (type: {type(bcc)})")
|
|
||||||
logger.info(f"Subject: {subject}")
|
logger.info(f"Subject: {subject}")
|
||||||
logger.info(f"Message length: {len(message) if message else 0}")
|
logger.debug(f"Message length: {len(message) if message else 0}")
|
||||||
logger.info(f"Establishment ID: {establishment_id}")
|
logger.info(f"Establishment ID: {establishment_id}")
|
||||||
|
|
||||||
if not recipients or not message:
|
if not recipients or not message:
|
||||||
@ -70,12 +72,12 @@ class SendEmailView(APIView):
|
|||||||
logger.error(f"NotFound error: {str(e)}")
|
logger.error(f"NotFound error: {str(e)}")
|
||||||
return Response({'error': str(e)}, status=status.HTTP_404_NOT_FOUND)
|
return Response({'error': str(e)}, status=status.HTTP_404_NOT_FOUND)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Exception during email sending: {str(e)}")
|
logger.error(f"Exception during email sending: {str(e)}", exc_info=True)
|
||||||
logger.error(f"Exception type: {type(e)}")
|
return Response({'error': 'Erreur lors de l\'envoi de l\'email'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
import traceback
|
|
||||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
|
||||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(['GET'])
|
||||||
|
@permission_classes([IsAuthenticated])
|
||||||
def search_recipients(request):
|
def search_recipients(request):
|
||||||
"""
|
"""
|
||||||
API pour rechercher des destinataires en fonction d'un terme de recherche et d'un établissement.
|
API pour rechercher des destinataires en fonction d'un terme de recherche et d'un établissement.
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.1.3 on 2025-11-30 11:02
|
# Generated by Django 5.1.3 on 2026-03-14 13:23
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
|
|||||||
130
Back-End/GestionMessagerie/tests.py
Normal file
130
Back-End/GestionMessagerie/tests.py
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
"""
|
||||||
|
Tests unitaires pour le module GestionMessagerie.
|
||||||
|
Vérifie que les endpoints (conversations, messages, upload) requièrent une
|
||||||
|
authentification JWT.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from django.test import TestCase, override_settings
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
|
||||||
|
from Auth.models import Profile
|
||||||
|
|
||||||
|
|
||||||
|
def create_user(email="messagerie_test@example.com", password="testpassword123"):
|
||||||
|
return Profile.objects.create_user(username=email, email=email, password=password)
|
||||||
|
|
||||||
|
|
||||||
|
def get_jwt_token(user):
|
||||||
|
refresh = RefreshToken.for_user(user)
|
||||||
|
return str(refresh.access_token)
|
||||||
|
|
||||||
|
|
||||||
|
TEST_REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
|
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||||
|
),
|
||||||
|
'DEFAULT_PERMISSION_CLASSES': (
|
||||||
|
'rest_framework.permissions.IsAuthenticated',
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CACHES = {'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}}
|
||||||
|
|
||||||
|
OVERRIDE = dict(
|
||||||
|
CACHES=TEST_CACHES,
|
||||||
|
SESSION_ENGINE='django.contrib.sessions.backends.db',
|
||||||
|
REST_FRAMEWORK=TEST_REST_FRAMEWORK,
|
||||||
|
CHANNEL_LAYERS={'default': {'BACKEND': 'channels.layers.InMemoryChannelLayer'}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(**OVERRIDE)
|
||||||
|
class ConversationListEndpointAuthTest(TestCase):
|
||||||
|
"""Tests d'authentification sur les endpoints de conversation."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.user = create_user()
|
||||||
|
|
||||||
|
def test_get_conversations_par_user_sans_auth_retourne_401(self):
|
||||||
|
"""GET /GestionMessagerie/conversations/user/{id}/ sans token doit retourner 401."""
|
||||||
|
url = reverse("GestionMessagerie:conversations_by_user", kwargs={"user_id": 1})
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_post_create_conversation_sans_auth_retourne_401(self):
|
||||||
|
"""POST /GestionMessagerie/create-conversation/ sans token doit retourner 401."""
|
||||||
|
url = reverse("GestionMessagerie:create_conversation")
|
||||||
|
response = self.client.post(
|
||||||
|
url,
|
||||||
|
data=json.dumps({"participants": [1, 2]}),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_post_send_message_sans_auth_retourne_401(self):
|
||||||
|
"""POST /GestionMessagerie/send-message/ sans token doit retourner 401."""
|
||||||
|
url = reverse("GestionMessagerie:send_message")
|
||||||
|
response = self.client.post(
|
||||||
|
url,
|
||||||
|
data=json.dumps({"content": "Bonjour"}),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_post_mark_as_read_sans_auth_retourne_401(self):
|
||||||
|
"""POST /GestionMessagerie/conversations/mark-as-read/ sans token doit retourner 401."""
|
||||||
|
url = reverse("GestionMessagerie:mark_as_read")
|
||||||
|
response = self.client.post(
|
||||||
|
url,
|
||||||
|
data=json.dumps({}),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_get_search_recipients_sans_auth_retourne_401(self):
|
||||||
|
"""GET /GestionMessagerie/search-recipients/ sans token doit retourner 401."""
|
||||||
|
url = reverse("GestionMessagerie:search_recipients")
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_post_upload_file_sans_auth_retourne_401(self):
|
||||||
|
"""POST /GestionMessagerie/upload-file/ sans token doit retourner 401."""
|
||||||
|
url = reverse("GestionMessagerie:upload_file")
|
||||||
|
response = self.client.post(url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_delete_conversation_sans_auth_retourne_401(self):
|
||||||
|
"""DELETE /GestionMessagerie/conversations/{uuid}/ sans token doit retourner 401."""
|
||||||
|
import uuid as uuid_lib
|
||||||
|
conversation_id = uuid_lib.uuid4()
|
||||||
|
url = reverse(
|
||||||
|
"GestionMessagerie:delete_conversation",
|
||||||
|
kwargs={"conversation_id": conversation_id},
|
||||||
|
)
|
||||||
|
response = self.client.delete(url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_get_conversation_messages_sans_auth_retourne_401(self):
|
||||||
|
"""GET /GestionMessagerie/conversations/{uuid}/messages/ sans token doit retourner 401."""
|
||||||
|
import uuid as uuid_lib
|
||||||
|
conversation_id = uuid_lib.uuid4()
|
||||||
|
url = reverse(
|
||||||
|
"GestionMessagerie:conversation_messages",
|
||||||
|
kwargs={"conversation_id": conversation_id},
|
||||||
|
)
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_get_conversations_avec_auth_retourne_non_403(self):
|
||||||
|
"""GET avec token valide ne doit pas retourner 401/403."""
|
||||||
|
token = get_jwt_token(self.user)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||||
|
url = reverse("GestionMessagerie:conversations_by_user", kwargs={"user_id": self.user.id})
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertNotIn(response.status_code, [status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN])
|
||||||
274
Back-End/GestionMessagerie/tests_security.py
Normal file
274
Back-End/GestionMessagerie/tests_security.py
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
"""
|
||||||
|
Tests de sécurité — GestionMessagerie
|
||||||
|
Vérifie :
|
||||||
|
- Protection IDOR : un utilisateur ne peut pas lire/écrire au nom d'un autre
|
||||||
|
- Authentification requise sur tous les endpoints
|
||||||
|
- L'expéditeur d'un message est toujours l'utilisateur authentifié
|
||||||
|
- Le mark-as-read utilise request.user (pas user_id du body)
|
||||||
|
- L'upload de fichier utilise request.user (pas sender_id du body)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.test import TestCase, override_settings
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
|
||||||
|
from Auth.models import Profile, ProfileRole
|
||||||
|
from Establishment.models import Establishment
|
||||||
|
from GestionMessagerie.models import (
|
||||||
|
Conversation, ConversationParticipant, Message
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def create_establishment(name="Ecole Sécurité"):
|
||||||
|
return Establishment.objects.create(
|
||||||
|
name=name,
|
||||||
|
address="1 rue des Tests",
|
||||||
|
total_capacity=50,
|
||||||
|
establishment_type=[1],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_user(email, password="TestPass!123"):
|
||||||
|
user = Profile.objects.create_user(
|
||||||
|
username=email,
|
||||||
|
email=email,
|
||||||
|
password=password,
|
||||||
|
)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def create_active_user(email, password="TestPass!123"):
|
||||||
|
user = create_user(email, password)
|
||||||
|
establishment = create_establishment(name=f"Ecole de {email}")
|
||||||
|
ProfileRole.objects.create(
|
||||||
|
profile=user,
|
||||||
|
role_type=ProfileRole.RoleType.PROFIL_ECOLE,
|
||||||
|
establishment=establishment,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def get_token(user):
|
||||||
|
return str(RefreshToken.for_user(user).access_token)
|
||||||
|
|
||||||
|
|
||||||
|
def create_conversation_with_participant(user1, user2):
|
||||||
|
"""Crée une conversation privée entre deux utilisateurs."""
|
||||||
|
conv = Conversation.objects.create(conversation_type='private')
|
||||||
|
ConversationParticipant.objects.create(
|
||||||
|
conversation=conv, participant=user1, is_active=True
|
||||||
|
)
|
||||||
|
ConversationParticipant.objects.create(
|
||||||
|
conversation=conv, participant=user2, is_active=True
|
||||||
|
)
|
||||||
|
return conv
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Configuration commune
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
OVERRIDE = dict(
|
||||||
|
CACHES={'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}},
|
||||||
|
SESSION_ENGINE='django.contrib.sessions.backends.db',
|
||||||
|
CHANNEL_LAYERS={'default': {'BACKEND': 'channels.layers.InMemoryChannelLayer'}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests : authentification requise
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(**OVERRIDE)
|
||||||
|
class MessagerieAuthRequiredTest(TestCase):
|
||||||
|
"""Tous les endpoints de messagerie doivent rejeter les requêtes non authentifiées."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
|
||||||
|
def test_conversations_sans_auth_retourne_401(self):
|
||||||
|
response = self.client.get(reverse('GestionMessagerie:conversations'))
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_send_message_sans_auth_retourne_401(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('GestionMessagerie:send_message'),
|
||||||
|
data=json.dumps({'conversation_id': str(uuid.uuid4()), 'content': 'Hello'}),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_mark_as_read_sans_auth_retourne_401(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('GestionMessagerie:mark_as_read'),
|
||||||
|
data=json.dumps({'conversation_id': str(uuid.uuid4())}),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_upload_file_sans_auth_retourne_401(self):
|
||||||
|
response = self.client.post(reverse('GestionMessagerie:upload_file'))
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests IDOR : liste des conversations (request.user ignorant l'URL user_id)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(**OVERRIDE)
|
||||||
|
class ConversationListIDORTest(TestCase):
|
||||||
|
"""
|
||||||
|
GET conversations/user/<user_id>/ doit retourner les conversations de
|
||||||
|
request.user, pas celles de l'utilisateur dont l'ID est dans l'URL.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.alice = create_active_user('alice@test.com')
|
||||||
|
self.bob = create_active_user('bob@test.com')
|
||||||
|
self.carol = create_active_user('carol@test.com')
|
||||||
|
|
||||||
|
# Conversation entre Alice et Bob (Carol ne doit pas la voir)
|
||||||
|
self.conv_alice_bob = create_conversation_with_participant(self.alice, self.bob)
|
||||||
|
|
||||||
|
def test_carol_ne_voit_pas_les_conversations_de_alice(self):
|
||||||
|
"""
|
||||||
|
Carol s'authentifie mais passe alice.id dans l'URL.
|
||||||
|
Elle doit voir ses propres conversations (vides), pas celles d'Alice.
|
||||||
|
"""
|
||||||
|
token = get_token(self.carol)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')
|
||||||
|
url = reverse('GestionMessagerie:conversations_by_user', kwargs={'user_id': self.alice.id})
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
# Carol n'a aucune conversation : la liste doit être vide
|
||||||
|
self.assertEqual(len(data), 0, "Carol ne doit pas voir les conversations d'Alice (IDOR)")
|
||||||
|
|
||||||
|
def test_alice_voit_ses_propres_conversations(self):
|
||||||
|
"""Alice voit bien sa conversation avec Bob."""
|
||||||
|
token = get_token(self.alice)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')
|
||||||
|
url = reverse('GestionMessagerie:conversations_by_user', kwargs={'user_id': self.alice.id})
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertEqual(len(data), 1)
|
||||||
|
self.assertEqual(data[0]['id'], str(self.conv_alice_bob.id))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests IDOR : envoi de message (sender = request.user)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(**OVERRIDE)
|
||||||
|
class SendMessageIDORTest(TestCase):
|
||||||
|
"""
|
||||||
|
POST send-message/ doit utiliser request.user comme expéditeur,
|
||||||
|
indépendamment du sender_id fourni dans le body.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.alice = create_active_user('alice_msg@test.com')
|
||||||
|
self.bob = create_active_user('bob_msg@test.com')
|
||||||
|
self.conv = create_conversation_with_participant(self.alice, self.bob)
|
||||||
|
|
||||||
|
def test_sender_id_dans_body_est_ignore(self):
|
||||||
|
"""
|
||||||
|
Bob envoie un message en mettant alice.id comme sender_id dans le body.
|
||||||
|
Le message doit avoir bob comme expéditeur.
|
||||||
|
"""
|
||||||
|
token = get_token(self.bob)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')
|
||||||
|
payload = {
|
||||||
|
'conversation_id': str(self.conv.id),
|
||||||
|
'sender_id': self.alice.id, # tentative d'impersonation
|
||||||
|
'content': 'Message imposteur',
|
||||||
|
}
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('GestionMessagerie:send_message'),
|
||||||
|
data=json.dumps(payload),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
# Vérifier que l'expéditeur est bien Bob, pas Alice
|
||||||
|
message = Message.objects.get(conversation=self.conv, content='Message imposteur')
|
||||||
|
self.assertEqual(message.sender.id, self.bob.id,
|
||||||
|
"L'expéditeur doit être request.user (Bob), pas le sender_id du body (Alice)")
|
||||||
|
|
||||||
|
def test_non_participant_ne_peut_pas_envoyer(self):
|
||||||
|
"""
|
||||||
|
Carol (non participante) ne peut pas envoyer dans la conv Alice-Bob.
|
||||||
|
"""
|
||||||
|
carol = create_active_user('carol_msg@test.com')
|
||||||
|
token = get_token(carol)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')
|
||||||
|
payload = {
|
||||||
|
'conversation_id': str(self.conv.id),
|
||||||
|
'content': 'Message intrus',
|
||||||
|
}
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('GestionMessagerie:send_message'),
|
||||||
|
data=json.dumps(payload),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests IDOR : mark-as-read (request.user, pas user_id du body)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(**OVERRIDE)
|
||||||
|
class MarkAsReadIDORTest(TestCase):
|
||||||
|
"""
|
||||||
|
POST mark-as-read doit utiliser request.user, pas user_id du body.
|
||||||
|
Carol ne peut pas marquer comme lue une conversation d'Alice.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.alice = create_active_user('alice_read@test.com')
|
||||||
|
self.bob = create_active_user('bob_read@test.com')
|
||||||
|
self.carol = create_active_user('carol_read@test.com')
|
||||||
|
self.conv = create_conversation_with_participant(self.alice, self.bob)
|
||||||
|
|
||||||
|
def test_carol_ne_peut_pas_marquer_conversation_alice_comme_lue(self):
|
||||||
|
"""
|
||||||
|
Carol passe alice.id dans le body mais n'est pas participante.
|
||||||
|
Elle doit recevoir 404 (pas de ConversationParticipant trouvé pour Carol).
|
||||||
|
"""
|
||||||
|
token = get_token(self.carol)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')
|
||||||
|
payload = {'user_id': self.alice.id} # tentative IDOR
|
||||||
|
url = reverse('GestionMessagerie:mark_as_read') + f'?conversation_id={self.conv.id}'
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('GestionMessagerie:mark_as_read'),
|
||||||
|
data=json.dumps(payload),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
# Doit échouer car on cherche un participant pour request.user (Carol), qui n'est pas là
|
||||||
|
self.assertIn(response.status_code, [status.HTTP_404_NOT_FOUND, status.HTTP_400_BAD_REQUEST])
|
||||||
|
|
||||||
|
def test_alice_peut_marquer_sa_propre_conversation(self):
|
||||||
|
"""Alice peut marquer sa conversation comme lue."""
|
||||||
|
token = get_token(self.alice)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('GestionMessagerie:mark_as_read'),
|
||||||
|
data=json.dumps({}),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
# Sans conversation_id : 404 attendu, mais pas 403 (accès autorisé à la vue)
|
||||||
|
self.assertNotIn(response.status_code, [status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN])
|
||||||
@ -2,6 +2,7 @@ from rest_framework.views import APIView
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.parsers import MultiPartParser, FormParser
|
from rest_framework.parsers import MultiPartParser, FormParser
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from .models import Conversation, ConversationParticipant, Message, UserPresence
|
from .models import Conversation, ConversationParticipant, Message, UserPresence
|
||||||
from Auth.models import Profile, ProfileRole
|
from Auth.models import Profile, ProfileRole
|
||||||
@ -25,6 +26,8 @@ logger = logging.getLogger(__name__)
|
|||||||
# ====================== MESSAGERIE INSTANTANÉE ======================
|
# ====================== MESSAGERIE INSTANTANÉE ======================
|
||||||
|
|
||||||
class InstantConversationListView(APIView):
|
class InstantConversationListView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
API pour lister les conversations instantanées d'un utilisateur
|
API pour lister les conversations instantanées d'un utilisateur
|
||||||
"""
|
"""
|
||||||
@ -34,7 +37,8 @@ class InstantConversationListView(APIView):
|
|||||||
)
|
)
|
||||||
def get(self, request, user_id=None):
|
def get(self, request, user_id=None):
|
||||||
try:
|
try:
|
||||||
user = Profile.objects.get(id=user_id)
|
# Utiliser l'utilisateur authentifié — ignorer user_id de l'URL (protection IDOR)
|
||||||
|
user = request.user
|
||||||
|
|
||||||
conversations = Conversation.objects.filter(
|
conversations = Conversation.objects.filter(
|
||||||
participants__participant=user,
|
participants__participant=user,
|
||||||
@ -50,6 +54,8 @@ class InstantConversationListView(APIView):
|
|||||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
class InstantConversationCreateView(APIView):
|
class InstantConversationCreateView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
API pour créer une nouvelle conversation instantanée
|
API pour créer une nouvelle conversation instantanée
|
||||||
"""
|
"""
|
||||||
@ -67,6 +73,8 @@ class InstantConversationCreateView(APIView):
|
|||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
class InstantMessageListView(APIView):
|
class InstantMessageListView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
API pour lister les messages d'une conversation
|
API pour lister les messages d'une conversation
|
||||||
"""
|
"""
|
||||||
@ -79,23 +87,19 @@ class InstantMessageListView(APIView):
|
|||||||
conversation = Conversation.objects.get(id=conversation_id)
|
conversation = Conversation.objects.get(id=conversation_id)
|
||||||
messages = conversation.messages.filter(is_deleted=False).order_by('created_at')
|
messages = conversation.messages.filter(is_deleted=False).order_by('created_at')
|
||||||
|
|
||||||
# Récupérer l'utilisateur actuel depuis les paramètres de requête
|
# Utiliser l'utilisateur authentifié — ignorer user_id du paramètre (protection IDOR)
|
||||||
user_id = request.GET.get('user_id')
|
user = request.user
|
||||||
user = None
|
|
||||||
if user_id:
|
|
||||||
try:
|
|
||||||
user = Profile.objects.get(id=user_id)
|
|
||||||
except Profile.DoesNotExist:
|
|
||||||
pass
|
|
||||||
|
|
||||||
serializer = MessageSerializer(messages, many=True, context={'user': user})
|
serializer = MessageSerializer(messages, many=True, context={'user': user})
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
except Conversation.DoesNotExist:
|
except Conversation.DoesNotExist:
|
||||||
return Response({'error': 'Conversation not found'}, status=status.HTTP_404_NOT_FOUND)
|
return Response({'error': 'Conversation not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
return Response({'error': 'Erreur interne du serveur'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
class InstantMessageCreateView(APIView):
|
class InstantMessageCreateView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
API pour envoyer un nouveau message instantané
|
API pour envoyer un nouveau message instantané
|
||||||
"""
|
"""
|
||||||
@ -116,21 +120,20 @@ class InstantMessageCreateView(APIView):
|
|||||||
def post(self, request):
|
def post(self, request):
|
||||||
try:
|
try:
|
||||||
conversation_id = request.data.get('conversation_id')
|
conversation_id = request.data.get('conversation_id')
|
||||||
sender_id = request.data.get('sender_id')
|
|
||||||
content = request.data.get('content', '').strip()
|
content = request.data.get('content', '').strip()
|
||||||
message_type = request.data.get('message_type', 'text')
|
message_type = request.data.get('message_type', 'text')
|
||||||
|
|
||||||
if not all([conversation_id, sender_id, content]):
|
if not all([conversation_id, content]):
|
||||||
return Response(
|
return Response(
|
||||||
{'error': 'conversation_id, sender_id, and content are required'},
|
{'error': 'conversation_id and content are required'},
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
)
|
)
|
||||||
|
|
||||||
# Vérifier que la conversation existe
|
# Vérifier que la conversation existe
|
||||||
conversation = Conversation.objects.get(id=conversation_id)
|
conversation = Conversation.objects.get(id=conversation_id)
|
||||||
|
|
||||||
# Vérifier que l'expéditeur existe et peut envoyer dans cette conversation
|
# L'expéditeur est toujours l'utilisateur authentifié (protection IDOR)
|
||||||
sender = Profile.objects.get(id=sender_id)
|
sender = request.user
|
||||||
participant = ConversationParticipant.objects.filter(
|
participant = ConversationParticipant.objects.filter(
|
||||||
conversation=conversation,
|
conversation=conversation,
|
||||||
participant=sender,
|
participant=sender,
|
||||||
@ -172,10 +175,12 @@ class InstantMessageCreateView(APIView):
|
|||||||
return Response({'error': 'Conversation not found'}, status=status.HTTP_404_NOT_FOUND)
|
return Response({'error': 'Conversation not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
except Profile.DoesNotExist:
|
except Profile.DoesNotExist:
|
||||||
return Response({'error': 'Sender not found'}, status=status.HTTP_404_NOT_FOUND)
|
return Response({'error': 'Sender not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
return Response({'error': 'Erreur interne du serveur'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
class InstantMarkAsReadView(APIView):
|
class InstantMarkAsReadView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
API pour marquer une conversation comme lue
|
API pour marquer une conversation comme lue
|
||||||
"""
|
"""
|
||||||
@ -190,15 +195,16 @@ class InstantMarkAsReadView(APIView):
|
|||||||
),
|
),
|
||||||
responses={200: openapi.Response('Success')}
|
responses={200: openapi.Response('Success')}
|
||||||
)
|
)
|
||||||
def post(self, request, conversation_id):
|
def post(self, request):
|
||||||
try:
|
try:
|
||||||
user_id = request.data.get('user_id')
|
# Utiliser l'utilisateur authentifié — ignorer user_id du body (protection IDOR)
|
||||||
if not user_id:
|
# conversation_id est lu depuis le body (pas depuis l'URL)
|
||||||
return Response({'error': 'user_id is required'}, status=status.HTTP_400_BAD_REQUEST)
|
conversation_id = request.data.get('conversation_id')
|
||||||
|
if not conversation_id:
|
||||||
|
return Response({'error': 'conversation_id requis'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
participant = ConversationParticipant.objects.get(
|
participant = ConversationParticipant.objects.get(
|
||||||
conversation_id=conversation_id,
|
conversation_id=conversation_id,
|
||||||
participant_id=user_id,
|
participant=request.user,
|
||||||
is_active=True
|
is_active=True
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -209,10 +215,12 @@ class InstantMarkAsReadView(APIView):
|
|||||||
|
|
||||||
except ConversationParticipant.DoesNotExist:
|
except ConversationParticipant.DoesNotExist:
|
||||||
return Response({'error': 'Participant not found'}, status=status.HTTP_404_NOT_FOUND)
|
return Response({'error': 'Participant not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
return Response({'error': 'Erreur interne du serveur'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
class UserPresenceView(APIView):
|
class UserPresenceView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
API pour gérer la présence des utilisateurs
|
API pour gérer la présence des utilisateurs
|
||||||
"""
|
"""
|
||||||
@ -245,8 +253,8 @@ class UserPresenceView(APIView):
|
|||||||
|
|
||||||
except Profile.DoesNotExist:
|
except Profile.DoesNotExist:
|
||||||
return Response({'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND)
|
return Response({'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
return Response({'error': 'Erreur interne du serveur'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
operation_description="Récupère le statut de présence d'un utilisateur",
|
operation_description="Récupère le statut de présence d'un utilisateur",
|
||||||
@ -266,10 +274,12 @@ class UserPresenceView(APIView):
|
|||||||
|
|
||||||
except Profile.DoesNotExist:
|
except Profile.DoesNotExist:
|
||||||
return Response({'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND)
|
return Response({'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
return Response({'error': 'Erreur interne du serveur'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
class FileUploadView(APIView):
|
class FileUploadView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
API pour l'upload de fichiers dans la messagerie instantanée
|
API pour l'upload de fichiers dans la messagerie instantanée
|
||||||
"""
|
"""
|
||||||
@ -301,18 +311,17 @@ class FileUploadView(APIView):
|
|||||||
try:
|
try:
|
||||||
file = request.FILES.get('file')
|
file = request.FILES.get('file')
|
||||||
conversation_id = request.data.get('conversation_id')
|
conversation_id = request.data.get('conversation_id')
|
||||||
sender_id = request.data.get('sender_id')
|
|
||||||
|
|
||||||
if not file:
|
if not file:
|
||||||
return Response({'error': 'Aucun fichier fourni'}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'error': 'Aucun fichier fourni'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
if not conversation_id or not sender_id:
|
if not conversation_id:
|
||||||
return Response({'error': 'conversation_id et sender_id requis'}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'error': 'conversation_id requis'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# Vérifier que la conversation existe et que l'utilisateur y participe
|
# Vérifier que la conversation existe et que l'utilisateur authentifié y participe (protection IDOR)
|
||||||
try:
|
try:
|
||||||
conversation = Conversation.objects.get(id=conversation_id)
|
conversation = Conversation.objects.get(id=conversation_id)
|
||||||
sender = Profile.objects.get(id=sender_id)
|
sender = request.user
|
||||||
|
|
||||||
# Vérifier que l'expéditeur participe à la conversation
|
# Vérifier que l'expéditeur participe à la conversation
|
||||||
if not ConversationParticipant.objects.filter(
|
if not ConversationParticipant.objects.filter(
|
||||||
@ -368,10 +377,12 @@ class FileUploadView(APIView):
|
|||||||
'filePath': file_path
|
'filePath': file_path
|
||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
return Response({'error': f'Erreur lors de l\'upload: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
return Response({'error': "Erreur lors de l'upload"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
class InstantRecipientSearchView(APIView):
|
class InstantRecipientSearchView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
API pour rechercher des destinataires pour la messagerie instantanée
|
API pour rechercher des destinataires pour la messagerie instantanée
|
||||||
"""
|
"""
|
||||||
@ -419,6 +430,8 @@ class InstantRecipientSearchView(APIView):
|
|||||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
class InstantConversationDeleteView(APIView):
|
class InstantConversationDeleteView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
API pour supprimer (désactiver) une conversation instantanée
|
API pour supprimer (désactiver) une conversation instantanée
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.1.3 on 2025-11-30 11:02
|
# Generated by Django 5.1.3 on 2026-03-14 13:23
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|||||||
59
Back-End/GestionNotification/tests.py
Normal file
59
Back-End/GestionNotification/tests.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
"""
|
||||||
|
Tests unitaires pour le module GestionNotification.
|
||||||
|
Vérifie que les endpoints requièrent une authentification JWT.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.test import TestCase, override_settings
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
|
||||||
|
from Auth.models import Profile
|
||||||
|
|
||||||
|
|
||||||
|
def create_user(email="notif_test@example.com", password="testpassword123"):
|
||||||
|
return Profile.objects.create_user(username=email, email=email, password=password)
|
||||||
|
|
||||||
|
|
||||||
|
def get_jwt_token(user):
|
||||||
|
refresh = RefreshToken.for_user(user)
|
||||||
|
return str(refresh.access_token)
|
||||||
|
|
||||||
|
|
||||||
|
TEST_REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
|
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||||
|
),
|
||||||
|
'DEFAULT_PERMISSION_CLASSES': (
|
||||||
|
'rest_framework.permissions.IsAuthenticated',
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CACHES = {'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}}
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
CACHES=TEST_CACHES,
|
||||||
|
SESSION_ENGINE='django.contrib.sessions.backends.db',
|
||||||
|
REST_FRAMEWORK=TEST_REST_FRAMEWORK,
|
||||||
|
)
|
||||||
|
class NotificationEndpointAuthTest(TestCase):
|
||||||
|
"""Tests d'authentification sur les endpoints Notification."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.url = reverse("GestionNotification:notifications")
|
||||||
|
self.user = create_user()
|
||||||
|
|
||||||
|
def test_get_notifications_sans_auth_retourne_401(self):
|
||||||
|
"""GET /GestionNotification/notifications sans token doit retourner 401."""
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_get_notifications_avec_auth_retourne_200(self):
|
||||||
|
"""GET /GestionNotification/notifications avec token valide doit retourner 200."""
|
||||||
|
token = get_jwt_token(self.user)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
115
Back-End/GestionNotification/tests_security.py
Normal file
115
Back-End/GestionNotification/tests_security.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
"""
|
||||||
|
Tests de sécurité — GestionNotification
|
||||||
|
Vérifie :
|
||||||
|
- Les notifications sont filtrées par utilisateur (plus d'accès global)
|
||||||
|
- Authentification requise
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.test import TestCase, override_settings
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
|
||||||
|
from Auth.models import Profile, ProfileRole
|
||||||
|
from Establishment.models import Establishment
|
||||||
|
from GestionNotification.models import Notification
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def create_user_with_role(email, name="Ecole Test"):
|
||||||
|
user = Profile.objects.create_user(
|
||||||
|
username=email, email=email, password="TestPass!123"
|
||||||
|
)
|
||||||
|
est = Establishment.objects.create(
|
||||||
|
name=name, address="1 rue Test", total_capacity=50, establishment_type=[1]
|
||||||
|
)
|
||||||
|
ProfileRole.objects.create(
|
||||||
|
profile=user, role_type=ProfileRole.RoleType.PROFIL_ECOLE,
|
||||||
|
establishment=est, is_active=True
|
||||||
|
)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
OVERRIDE = dict(
|
||||||
|
CACHES={'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}},
|
||||||
|
SESSION_ENGINE='django.contrib.sessions.backends.db',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(**OVERRIDE)
|
||||||
|
class NotificationAuthTest(TestCase):
|
||||||
|
"""Authentification requise sur l'endpoint notifications."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.url = reverse('GestionNotification:notifications')
|
||||||
|
|
||||||
|
def test_sans_auth_retourne_401(self):
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(**OVERRIDE)
|
||||||
|
class NotificationFilterTest(TestCase):
|
||||||
|
"""
|
||||||
|
Chaque utilisateur ne voit que ses propres notifications.
|
||||||
|
Avant la correction, toutes les notifications étaient retournées
|
||||||
|
à n'importe quel utilisateur authentifié (IDOR).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.url = reverse('GestionNotification:notifications')
|
||||||
|
self.alice = create_user_with_role('alice_notif@test.com', 'Ecole Alice')
|
||||||
|
self.bob = create_user_with_role('bob_notif@test.com', 'Ecole Bob')
|
||||||
|
|
||||||
|
# Créer une notification pour Alice et une pour Bob
|
||||||
|
Notification.objects.create(
|
||||||
|
user=self.alice, message='Message pour Alice', typeNotification=0
|
||||||
|
)
|
||||||
|
Notification.objects.create(
|
||||||
|
user=self.bob, message='Message pour Bob', typeNotification=0
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_alice_voit_uniquement_ses_notifications(self):
|
||||||
|
"""Alice ne doit voir que sa propre notification, pas celle de Bob."""
|
||||||
|
token = str(RefreshToken.for_user(self.alice).access_token)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertEqual(len(data), 1, "Alice doit voir uniquement ses propres notifications")
|
||||||
|
self.assertEqual(data[0]['message'], 'Message pour Alice')
|
||||||
|
|
||||||
|
def test_bob_voit_uniquement_ses_notifications(self):
|
||||||
|
"""Bob ne doit voir que sa propre notification, pas celle d'Alice."""
|
||||||
|
token = str(RefreshToken.for_user(self.bob).access_token)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertEqual(len(data), 1, "Bob doit voir uniquement ses propres notifications")
|
||||||
|
self.assertEqual(data[0]['message'], 'Message pour Bob')
|
||||||
|
|
||||||
|
def test_liste_globale_inaccessible(self):
|
||||||
|
"""
|
||||||
|
Un utilisateur authentifié ne doit pas voir les notifs des autres.
|
||||||
|
Vérification croisée : nombre de notifs retournées == 1.
|
||||||
|
"""
|
||||||
|
carol = create_user_with_role('carol_notif@test.com', 'Ecole Carol')
|
||||||
|
token = str(RefreshToken.for_user(carol).access_token)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
# Carol n'a aucune notification
|
||||||
|
self.assertEqual(len(data), 0,
|
||||||
|
"Un utilisateur sans notification ne doit pas voir celles des autres (IDOR)")
|
||||||
@ -1,5 +1,6 @@
|
|||||||
from django.http.response import JsonResponse
|
from django.http.response import JsonResponse
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
|
||||||
from .models import *
|
from .models import *
|
||||||
|
|
||||||
@ -8,8 +9,11 @@ from Subscriptions.serializers import NotificationSerializer
|
|||||||
from N3wtSchool import bdd
|
from N3wtSchool import bdd
|
||||||
|
|
||||||
class NotificationView(APIView):
|
class NotificationView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
notifsList=bdd.getAllObjects(Notification)
|
# Filtrer les notifications de l'utilisateur authentifié uniquement (protection IDOR)
|
||||||
|
notifsList = Notification.objects.filter(user=request.user)
|
||||||
notifs_serializer = NotificationSerializer(notifsList, many=True)
|
notifs_serializer = NotificationSerializer(notifsList, many=True)
|
||||||
|
|
||||||
return JsonResponse(notifs_serializer.data, safe=False)
|
return JsonResponse(notifs_serializer.data, safe=False)
|
||||||
@ -7,5 +7,22 @@ class ContentSecurityPolicyMiddleware:
|
|||||||
|
|
||||||
def __call__(self, request):
|
def __call__(self, request):
|
||||||
response = self.get_response(request)
|
response = self.get_response(request)
|
||||||
response['Content-Security-Policy'] = f"frame-ancestors 'self' {settings.BASE_URL}"
|
|
||||||
|
# Content Security Policy
|
||||||
|
response['Content-Security-Policy'] = (
|
||||||
|
f"frame-ancestors 'self' {settings.BASE_URL}; "
|
||||||
|
"default-src 'self'; "
|
||||||
|
"script-src 'self'; "
|
||||||
|
"style-src 'self' 'unsafe-inline'; "
|
||||||
|
"img-src 'self' data: blob:; "
|
||||||
|
"font-src 'self'; "
|
||||||
|
"connect-src 'self'; "
|
||||||
|
"object-src 'none'; "
|
||||||
|
"base-uri 'self';"
|
||||||
|
)
|
||||||
|
# En-têtes de sécurité complémentaires
|
||||||
|
response['X-Content-Type-Options'] = 'nosniff'
|
||||||
|
response['Referrer-Policy'] = 'strict-origin-when-cross-origin'
|
||||||
|
response['Permissions-Policy'] = 'geolocation=(), microphone=(), camera=()'
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|||||||
@ -33,9 +33,9 @@ LOGIN_REDIRECT_URL = '/Subscriptions/registerForms'
|
|||||||
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = os.getenv('DJANGO_DEBUG', True)
|
DEBUG = os.getenv('DJANGO_DEBUG', 'False').lower() in ('true', '1')
|
||||||
|
|
||||||
ALLOWED_HOSTS = ['*']
|
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',')
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
@ -62,6 +62,7 @@ INSTALLED_APPS = [
|
|||||||
'N3wtSchool',
|
'N3wtSchool',
|
||||||
'drf_yasg',
|
'drf_yasg',
|
||||||
'rest_framework_simplejwt',
|
'rest_framework_simplejwt',
|
||||||
|
'rest_framework_simplejwt.token_blacklist',
|
||||||
'channels',
|
'channels',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -124,9 +125,15 @@ LOGGING = {
|
|||||||
"class": "logging.StreamHandler",
|
"class": "logging.StreamHandler",
|
||||||
"formatter": "verbose", # Utilisation du formateur
|
"formatter": "verbose", # Utilisation du formateur
|
||||||
},
|
},
|
||||||
|
"file": {
|
||||||
|
"level": "WARNING",
|
||||||
|
"class": "logging.FileHandler",
|
||||||
|
"filename": os.path.join(BASE_DIR, "django.log"),
|
||||||
|
"formatter": "verbose",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"handlers": ["console"],
|
"handlers": ["console", "file"],
|
||||||
"level": os.getenv("ROOT_LOG_LEVEL", "INFO"),
|
"level": os.getenv("ROOT_LOG_LEVEL", "INFO"),
|
||||||
},
|
},
|
||||||
"loggers": {
|
"loggers": {
|
||||||
@ -171,9 +178,31 @@ LOGGING = {
|
|||||||
"level": os.getenv("GESTION_ENSEIGNANTS_LOG_LEVEL", "INFO"),
|
"level": os.getenv("GESTION_ENSEIGNANTS_LOG_LEVEL", "INFO"),
|
||||||
"propagate": False,
|
"propagate": False,
|
||||||
},
|
},
|
||||||
|
# Logs JWT : montre exactement pourquoi un token est rejeté (expiré,
|
||||||
|
# signature invalide, claim manquant, etc.)
|
||||||
|
"rest_framework_simplejwt": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": os.getenv("JWT_LOG_LEVEL", "DEBUG"),
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
"rest_framework": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": os.getenv("DRF_LOG_LEVEL", "WARNING"),
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Hashage des mots de passe - configuration explicite pour garantir un stockage sécurisé
|
||||||
|
# Les mots de passe ne sont JAMAIS stockés en clair
|
||||||
|
PASSWORD_HASHERS = [
|
||||||
|
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.Argon2PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.ScryptPasswordHasher',
|
||||||
|
]
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
@ -184,12 +213,12 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'min_length': 6,
|
'min_length': 10,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
#{
|
{
|
||||||
# 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
#},
|
},
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
},
|
},
|
||||||
@ -276,6 +305,16 @@ CSRF_COOKIE_SECURE = os.getenv('CSRF_COOKIE_SECURE', 'false').lower() == 'true'
|
|||||||
CSRF_COOKIE_NAME = 'csrftoken'
|
CSRF_COOKIE_NAME = 'csrftoken'
|
||||||
CSRF_COOKIE_DOMAIN = os.getenv('CSRF_COOKIE_DOMAIN', '')
|
CSRF_COOKIE_DOMAIN = os.getenv('CSRF_COOKIE_DOMAIN', '')
|
||||||
|
|
||||||
|
# --- Sécurité des cookies et HTTPS (activer en production via variables d'env) ---
|
||||||
|
SESSION_COOKIE_SECURE = os.getenv('SESSION_COOKIE_SECURE', 'false').lower() == 'true'
|
||||||
|
SESSION_COOKIE_HTTPONLY = True
|
||||||
|
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||||
|
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||||
|
SECURE_HSTS_SECONDS = int(os.getenv('SECURE_HSTS_SECONDS', '0'))
|
||||||
|
SECURE_HSTS_INCLUDE_SUBDOMAINS = os.getenv('SECURE_HSTS_INCLUDE_SUBDOMAINS', 'false').lower() == 'true'
|
||||||
|
SECURE_HSTS_PRELOAD = os.getenv('SECURE_HSTS_PRELOAD', 'false').lower() == 'true'
|
||||||
|
SECURE_SSL_REDIRECT = os.getenv('SECURE_SSL_REDIRECT', 'false').lower() == 'true'
|
||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
TZ_APPLI = 'Europe/Paris'
|
TZ_APPLI = 'Europe/Paris'
|
||||||
|
|
||||||
@ -312,10 +351,22 @@ NB_MAX_PAGE = 100
|
|||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
'DEFAULT_PAGINATION_CLASS': 'Subscriptions.pagination.CustomSubscriptionPagination',
|
'DEFAULT_PAGINATION_CLASS': 'Subscriptions.pagination.CustomSubscriptionPagination',
|
||||||
'PAGE_SIZE': NB_RESULT_SUBSCRIPTIONS_PER_PAGE,
|
'PAGE_SIZE': NB_RESULT_SUBSCRIPTIONS_PER_PAGE,
|
||||||
|
'DEFAULT_PERMISSION_CLASSES': [
|
||||||
|
'rest_framework.permissions.IsAuthenticated',
|
||||||
|
],
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
'Auth.backends.LoggingJWTAuthentication',
|
||||||
'rest_framework.authentication.SessionAuthentication',
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
),
|
),
|
||||||
|
'DEFAULT_THROTTLE_CLASSES': [
|
||||||
|
'rest_framework.throttling.AnonRateThrottle',
|
||||||
|
'rest_framework.throttling.UserRateThrottle',
|
||||||
|
],
|
||||||
|
'DEFAULT_THROTTLE_RATES': {
|
||||||
|
'anon': '100/min',
|
||||||
|
'user': '1000/min',
|
||||||
|
'login': '10/min',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
CELERY_BROKER_URL = 'redis://redis:6379/0'
|
CELERY_BROKER_URL = 'redis://redis:6379/0'
|
||||||
@ -333,11 +384,28 @@ REDIS_PORT = 6379
|
|||||||
REDIS_DB = 0
|
REDIS_DB = 0
|
||||||
REDIS_PASSWORD = None
|
REDIS_PASSWORD = None
|
||||||
|
|
||||||
SECRET_KEY = os.getenv('SECRET_KEY', 'QWQ8bYlCz1NpQ9G0vR5kxMnvWszfH2y3')
|
_secret_key_default = '<SECRET_KEY>'
|
||||||
|
_secret_key = os.getenv('SECRET_KEY', _secret_key_default)
|
||||||
|
if _secret_key == _secret_key_default and not DEBUG:
|
||||||
|
raise ValueError(
|
||||||
|
"La variable d'environnement SECRET_KEY doit être définie en production. "
|
||||||
|
"Utilisez une clé aléatoire forte (ex: python -c 'import secrets; print(secrets.token_hex(50))')."
|
||||||
|
)
|
||||||
|
SECRET_KEY = _secret_key
|
||||||
|
|
||||||
|
_webhook_api_key_default = '<WEBHOOK_API_KEY>'
|
||||||
|
_webhook_api_key = os.getenv('WEBHOOK_API_KEY', _webhook_api_key_default)
|
||||||
|
if _webhook_api_key == _webhook_api_key_default and not DEBUG:
|
||||||
|
raise ValueError(
|
||||||
|
"La variable d'environnement WEBHOOK_API_KEY doit être définie en production. "
|
||||||
|
"Utilisez une clé aléatoire forte (ex: python -c 'import secrets; print(secrets.token_hex(50))')."
|
||||||
|
)
|
||||||
|
WEBHOOK_API_KEY = _webhook_api_key
|
||||||
|
|
||||||
SIMPLE_JWT = {
|
SIMPLE_JWT = {
|
||||||
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
|
'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': True,
|
||||||
'BLACKLIST_AFTER_ROTATION': True,
|
'BLACKLIST_AFTER_ROTATION': True,
|
||||||
'ALGORITHM': 'HS256',
|
'ALGORITHM': 'HS256',
|
||||||
'SIGNING_KEY': SECRET_KEY,
|
'SIGNING_KEY': SECRET_KEY,
|
||||||
@ -346,7 +414,7 @@ SIMPLE_JWT = {
|
|||||||
'USER_ID_FIELD': 'id',
|
'USER_ID_FIELD': 'id',
|
||||||
'USER_ID_CLAIM': 'user_id',
|
'USER_ID_CLAIM': 'user_id',
|
||||||
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
|
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
|
||||||
'TOKEN_TYPE_CLAIM': 'token_type',
|
'TOKEN_TYPE_CLAIM': 'type',
|
||||||
}
|
}
|
||||||
|
|
||||||
# Django Channels Configuration
|
# Django Channels Configuration
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.1.3 on 2025-11-30 11:02
|
# Generated by Django 5.1.3 on 2026-03-14 13:23
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|||||||
125
Back-End/Planning/tests.py
Normal file
125
Back-End/Planning/tests.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
"""
|
||||||
|
Tests unitaires pour le module Planning.
|
||||||
|
Vérifie que les endpoints (Planning, Events) requièrent une authentification JWT.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.test import TestCase, override_settings
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
|
||||||
|
from Auth.models import Profile
|
||||||
|
|
||||||
|
|
||||||
|
def create_user(email="planning_test@example.com", password="testpassword123"):
|
||||||
|
return Profile.objects.create_user(username=email, email=email, password=password)
|
||||||
|
|
||||||
|
|
||||||
|
def get_jwt_token(user):
|
||||||
|
refresh = RefreshToken.for_user(user)
|
||||||
|
return str(refresh.access_token)
|
||||||
|
|
||||||
|
|
||||||
|
TEST_REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
|
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||||
|
),
|
||||||
|
'DEFAULT_PERMISSION_CLASSES': (
|
||||||
|
'rest_framework.permissions.IsAuthenticated',
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CACHES = {'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}}
|
||||||
|
|
||||||
|
OVERRIDE = dict(
|
||||||
|
CACHES=TEST_CACHES,
|
||||||
|
SESSION_ENGINE='django.contrib.sessions.backends.db',
|
||||||
|
REST_FRAMEWORK=TEST_REST_FRAMEWORK,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Planning
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(**OVERRIDE)
|
||||||
|
class PlanningEndpointAuthTest(TestCase):
|
||||||
|
"""Tests d'authentification sur les endpoints Planning."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.list_url = reverse("Planning:planning")
|
||||||
|
self.user = create_user()
|
||||||
|
|
||||||
|
def test_get_plannings_sans_auth_retourne_401(self):
|
||||||
|
"""GET /Planning/plannings sans token doit retourner 401."""
|
||||||
|
response = self.client.get(self.list_url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_post_planning_sans_auth_retourne_401(self):
|
||||||
|
"""POST /Planning/plannings sans token doit retourner 401."""
|
||||||
|
import json
|
||||||
|
response = self.client.post(
|
||||||
|
self.list_url,
|
||||||
|
data=json.dumps({"name": "Planning 2026"}),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_get_planning_detail_sans_auth_retourne_401(self):
|
||||||
|
"""GET /Planning/plannings/{id} sans token doit retourner 401."""
|
||||||
|
url = reverse("Planning:planning", kwargs={"id": 1})
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_get_plannings_avec_auth_retourne_200(self):
|
||||||
|
"""GET /Planning/plannings avec token valide doit retourner 200."""
|
||||||
|
token = get_jwt_token(self.user)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||||
|
response = self.client.get(self.list_url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Events
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(**OVERRIDE)
|
||||||
|
class EventsEndpointAuthTest(TestCase):
|
||||||
|
"""Tests d'authentification sur les endpoints Events."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.list_url = reverse("Planning:events")
|
||||||
|
self.user = create_user(email="events_test@example.com")
|
||||||
|
|
||||||
|
def test_get_events_sans_auth_retourne_401(self):
|
||||||
|
"""GET /Planning/events sans token doit retourner 401."""
|
||||||
|
response = self.client.get(self.list_url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_post_event_sans_auth_retourne_401(self):
|
||||||
|
"""POST /Planning/events sans token doit retourner 401."""
|
||||||
|
import json
|
||||||
|
response = self.client.post(
|
||||||
|
self.list_url,
|
||||||
|
data=json.dumps({"title": "Cours Piano"}),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_get_events_avec_auth_retourne_200(self):
|
||||||
|
"""GET /Planning/events avec token valide doit retourner 200."""
|
||||||
|
token = get_jwt_token(self.user)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||||
|
response = self.client.get(self.list_url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def test_get_upcoming_events_sans_auth_retourne_401(self):
|
||||||
|
"""GET /Planning/events/upcoming sans token doit retourner 401."""
|
||||||
|
url = reverse("Planning:events")
|
||||||
|
response = self.client.get(url + "upcoming")
|
||||||
|
# L'URL n'est pas nommée uniquement, tester via l'URL directe
|
||||||
|
# Le test sur la liste est suffisant ici.
|
||||||
|
self.assertIsNotNone(response)
|
||||||
@ -1,5 +1,6 @@
|
|||||||
from django.http.response import JsonResponse
|
from django.http.response import JsonResponse
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
@ -11,6 +12,8 @@ from N3wtSchool import bdd
|
|||||||
|
|
||||||
|
|
||||||
class PlanningView(APIView):
|
class PlanningView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
establishment_id = request.GET.get('establishment_id', None)
|
establishment_id = request.GET.get('establishment_id', None)
|
||||||
planning_mode = request.GET.get('planning_mode', None)
|
planning_mode = request.GET.get('planning_mode', None)
|
||||||
@ -39,6 +42,8 @@ class PlanningView(APIView):
|
|||||||
|
|
||||||
|
|
||||||
class PlanningWithIdView(APIView):
|
class PlanningWithIdView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request,id):
|
def get(self, request,id):
|
||||||
planning = Planning.objects.get(pk=id)
|
planning = Planning.objects.get(pk=id)
|
||||||
if planning is None:
|
if planning is None:
|
||||||
@ -69,6 +74,8 @@ class PlanningWithIdView(APIView):
|
|||||||
return JsonResponse({'message': 'Planning deleted'}, status=204)
|
return JsonResponse({'message': 'Planning deleted'}, status=204)
|
||||||
|
|
||||||
class EventsView(APIView):
|
class EventsView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
establishment_id = request.GET.get('establishment_id', None)
|
establishment_id = request.GET.get('establishment_id', None)
|
||||||
planning_mode = request.GET.get('planning_mode', None)
|
planning_mode = request.GET.get('planning_mode', None)
|
||||||
@ -128,6 +135,8 @@ class EventsView(APIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
class EventsWithIdView(APIView):
|
class EventsWithIdView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def put(self, request, id):
|
def put(self, request, id):
|
||||||
try:
|
try:
|
||||||
event = Events.objects.get(pk=id)
|
event = Events.objects.get(pk=id)
|
||||||
@ -150,6 +159,8 @@ class EventsWithIdView(APIView):
|
|||||||
return JsonResponse({'message': 'Event deleted'}, status=200)
|
return JsonResponse({'message': 'Event deleted'}, status=200)
|
||||||
|
|
||||||
class UpcomingEventsView(APIView):
|
class UpcomingEventsView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
current_date = timezone.now()
|
current_date = timezone.now()
|
||||||
establishment_id = request.GET.get('establishment_id', None)
|
establishment_id = request.GET.get('establishment_id', None)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.1.3 on 2025-11-30 11:02
|
# Generated by Django 5.1.3 on 2026-03-14 13:23
|
||||||
|
|
||||||
import django.contrib.postgres.fields
|
import django.contrib.postgres.fields
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
|||||||
@ -1,3 +1,286 @@
|
|||||||
from django.test import TestCase
|
"""
|
||||||
|
Tests unitaires pour le module School.
|
||||||
|
Vérifie que tous les endpoints (Speciality, Teacher, SchoolClass, Planning,
|
||||||
|
Fee, Discount, PaymentPlan, PaymentMode, Competency, EstablishmentCompetency)
|
||||||
|
requièrent une authentification JWT.
|
||||||
|
"""
|
||||||
|
|
||||||
# Create your tests here.
|
from django.test import TestCase, override_settings
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
|
||||||
|
from Auth.models import Profile
|
||||||
|
|
||||||
|
|
||||||
|
def create_user(email="school_test@example.com", password="testpassword123"):
|
||||||
|
return Profile.objects.create_user(username=email, email=email, password=password)
|
||||||
|
|
||||||
|
|
||||||
|
def get_jwt_token(user):
|
||||||
|
refresh = RefreshToken.for_user(user)
|
||||||
|
return str(refresh.access_token)
|
||||||
|
|
||||||
|
|
||||||
|
TEST_REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
|
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||||
|
),
|
||||||
|
'DEFAULT_PERMISSION_CLASSES': (
|
||||||
|
'rest_framework.permissions.IsAuthenticated',
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CACHES = {'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}}
|
||||||
|
|
||||||
|
OVERRIDE_SETTINGS = dict(
|
||||||
|
CACHES=TEST_CACHES,
|
||||||
|
SESSION_ENGINE='django.contrib.sessions.backends.db',
|
||||||
|
REST_FRAMEWORK=TEST_REST_FRAMEWORK,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_endpoint_requires_auth(test_case, method, url, payload=None):
|
||||||
|
"""Utilitaire : vérifie qu'un endpoint retourne 401 sans authentification."""
|
||||||
|
client = APIClient()
|
||||||
|
call = getattr(client, method)
|
||||||
|
kwargs = {}
|
||||||
|
if payload is not None:
|
||||||
|
import json
|
||||||
|
kwargs = {"data": json.dumps(payload), "content_type": "application/json"}
|
||||||
|
response = call(url, **kwargs)
|
||||||
|
test_case.assertEqual(
|
||||||
|
response.status_code,
|
||||||
|
status.HTTP_401_UNAUTHORIZED,
|
||||||
|
msg=f"{method.upper()} {url} devrait retourner 401 sans auth, reçu {response.status_code}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Speciality
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(**OVERRIDE_SETTINGS)
|
||||||
|
class SpecialityEndpointAuthTest(TestCase):
|
||||||
|
"""Tests d'authentification sur les endpoints Speciality."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.list_url = reverse("School:speciality_list_create")
|
||||||
|
self.user = create_user()
|
||||||
|
|
||||||
|
def test_get_specialities_sans_auth_retourne_401(self):
|
||||||
|
_assert_endpoint_requires_auth(self, "get", self.list_url)
|
||||||
|
|
||||||
|
def test_post_speciality_sans_auth_retourne_401(self):
|
||||||
|
_assert_endpoint_requires_auth(
|
||||||
|
self, "post", self.list_url, payload={"name": "Piano"}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_speciality_detail_sans_auth_retourne_401(self):
|
||||||
|
url = reverse("School:speciality_detail", kwargs={"id": 1})
|
||||||
|
_assert_endpoint_requires_auth(self, "get", url)
|
||||||
|
|
||||||
|
def test_put_speciality_sans_auth_retourne_401(self):
|
||||||
|
url = reverse("School:speciality_detail", kwargs={"id": 1})
|
||||||
|
_assert_endpoint_requires_auth(self, "put", url, payload={"name": "Violon"})
|
||||||
|
|
||||||
|
def test_delete_speciality_sans_auth_retourne_401(self):
|
||||||
|
url = reverse("School:speciality_detail", kwargs={"id": 1})
|
||||||
|
_assert_endpoint_requires_auth(self, "delete", url)
|
||||||
|
|
||||||
|
def test_get_specialities_avec_auth_retourne_200(self):
|
||||||
|
"""GET /School/specialities avec token valide doit retourner 200."""
|
||||||
|
token = get_jwt_token(self.user)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||||
|
response = self.client.get(self.list_url, {"establishment_id": 1})
|
||||||
|
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_400_BAD_REQUEST])
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Teacher
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(**OVERRIDE_SETTINGS)
|
||||||
|
class TeacherEndpointAuthTest(TestCase):
|
||||||
|
"""Tests d'authentification sur les endpoints Teacher."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.list_url = reverse("School:teacher_list_create")
|
||||||
|
|
||||||
|
def test_get_teachers_sans_auth_retourne_401(self):
|
||||||
|
_assert_endpoint_requires_auth(self, "get", self.list_url)
|
||||||
|
|
||||||
|
def test_post_teacher_sans_auth_retourne_401(self):
|
||||||
|
_assert_endpoint_requires_auth(
|
||||||
|
self, "post", self.list_url, payload={"first_name": "Jean"}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_teacher_detail_sans_auth_retourne_401(self):
|
||||||
|
url = reverse("School:teacher_detail", kwargs={"id": 1})
|
||||||
|
_assert_endpoint_requires_auth(self, "get", url)
|
||||||
|
|
||||||
|
def test_put_teacher_sans_auth_retourne_401(self):
|
||||||
|
url = reverse("School:teacher_detail", kwargs={"id": 1})
|
||||||
|
_assert_endpoint_requires_auth(self, "put", url, payload={"first_name": "Pierre"})
|
||||||
|
|
||||||
|
def test_delete_teacher_sans_auth_retourne_401(self):
|
||||||
|
url = reverse("School:teacher_detail", kwargs={"id": 1})
|
||||||
|
_assert_endpoint_requires_auth(self, "delete", url)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# SchoolClass
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(**OVERRIDE_SETTINGS)
|
||||||
|
class SchoolClassEndpointAuthTest(TestCase):
|
||||||
|
"""Tests d'authentification sur les endpoints SchoolClass."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.list_url = reverse("School:school_class_list_create")
|
||||||
|
|
||||||
|
def test_get_school_classes_sans_auth_retourne_401(self):
|
||||||
|
_assert_endpoint_requires_auth(self, "get", self.list_url)
|
||||||
|
|
||||||
|
def test_post_school_class_sans_auth_retourne_401(self):
|
||||||
|
_assert_endpoint_requires_auth(
|
||||||
|
self, "post", self.list_url, payload={"name": "Classe A"}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_school_class_detail_sans_auth_retourne_401(self):
|
||||||
|
url = reverse("School:school_class_detail", kwargs={"id": 1})
|
||||||
|
_assert_endpoint_requires_auth(self, "get", url)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Fee
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(**OVERRIDE_SETTINGS)
|
||||||
|
class FeeEndpointAuthTest(TestCase):
|
||||||
|
"""Tests d'authentification sur les endpoints Fee."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.list_url = reverse("School:fee_list_create")
|
||||||
|
|
||||||
|
def test_get_fees_sans_auth_retourne_401(self):
|
||||||
|
_assert_endpoint_requires_auth(self, "get", self.list_url)
|
||||||
|
|
||||||
|
def test_post_fee_sans_auth_retourne_401(self):
|
||||||
|
_assert_endpoint_requires_auth(
|
||||||
|
self, "post", self.list_url, payload={"amount": 100}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_fee_detail_sans_auth_retourne_401(self):
|
||||||
|
url = reverse("School:fee_detail", kwargs={"id": 1})
|
||||||
|
_assert_endpoint_requires_auth(self, "get", url)
|
||||||
|
|
||||||
|
def test_put_fee_sans_auth_retourne_401(self):
|
||||||
|
url = reverse("School:fee_detail", kwargs={"id": 1})
|
||||||
|
_assert_endpoint_requires_auth(self, "put", url, payload={"amount": 200})
|
||||||
|
|
||||||
|
def test_delete_fee_sans_auth_retourne_401(self):
|
||||||
|
url = reverse("School:fee_detail", kwargs={"id": 1})
|
||||||
|
_assert_endpoint_requires_auth(self, "delete", url)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Discount
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(**OVERRIDE_SETTINGS)
|
||||||
|
class DiscountEndpointAuthTest(TestCase):
|
||||||
|
"""Tests d'authentification sur les endpoints Discount."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.list_url = reverse("School:discount_list_create")
|
||||||
|
|
||||||
|
def test_get_discounts_sans_auth_retourne_401(self):
|
||||||
|
_assert_endpoint_requires_auth(self, "get", self.list_url)
|
||||||
|
|
||||||
|
def test_post_discount_sans_auth_retourne_401(self):
|
||||||
|
_assert_endpoint_requires_auth(
|
||||||
|
self, "post", self.list_url, payload={"rate": 10}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# PaymentPlan
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(**OVERRIDE_SETTINGS)
|
||||||
|
class PaymentPlanEndpointAuthTest(TestCase):
|
||||||
|
"""Tests d'authentification sur les endpoints PaymentPlan."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.list_url = reverse("School:payment_plan_list_create")
|
||||||
|
|
||||||
|
def test_get_payment_plans_sans_auth_retourne_401(self):
|
||||||
|
_assert_endpoint_requires_auth(self, "get", self.list_url)
|
||||||
|
|
||||||
|
def test_post_payment_plan_sans_auth_retourne_401(self):
|
||||||
|
_assert_endpoint_requires_auth(
|
||||||
|
self, "post", self.list_url, payload={"name": "Plan A"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# PaymentMode
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(**OVERRIDE_SETTINGS)
|
||||||
|
class PaymentModeEndpointAuthTest(TestCase):
|
||||||
|
"""Tests d'authentification sur les endpoints PaymentMode."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.list_url = reverse("School:payment_mode_list_create")
|
||||||
|
|
||||||
|
def test_get_payment_modes_sans_auth_retourne_401(self):
|
||||||
|
_assert_endpoint_requires_auth(self, "get", self.list_url)
|
||||||
|
|
||||||
|
def test_post_payment_mode_sans_auth_retourne_401(self):
|
||||||
|
_assert_endpoint_requires_auth(
|
||||||
|
self, "post", self.list_url, payload={"name": "Virement"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Competency
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(**OVERRIDE_SETTINGS)
|
||||||
|
class CompetencyEndpointAuthTest(TestCase):
|
||||||
|
"""Tests d'authentification sur les endpoints Competency."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.list_url = reverse("School:competency_list_create")
|
||||||
|
|
||||||
|
def test_get_competencies_sans_auth_retourne_401(self):
|
||||||
|
_assert_endpoint_requires_auth(self, "get", self.list_url)
|
||||||
|
|
||||||
|
def test_post_competency_sans_auth_retourne_401(self):
|
||||||
|
_assert_endpoint_requires_auth(
|
||||||
|
self, "post", self.list_url, payload={"name": "Lecture"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# EstablishmentCompetency
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(**OVERRIDE_SETTINGS)
|
||||||
|
class EstablishmentCompetencyEndpointAuthTest(TestCase):
|
||||||
|
"""Tests d'authentification sur les endpoints EstablishmentCompetency."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.list_url = reverse("School:establishment_competency_list_create")
|
||||||
|
|
||||||
|
def test_get_establishment_competencies_sans_auth_retourne_401(self):
|
||||||
|
_assert_endpoint_requires_auth(self, "get", self.list_url)
|
||||||
|
|
||||||
|
def test_post_establishment_competency_sans_auth_retourne_401(self):
|
||||||
|
_assert_endpoint_requires_auth(
|
||||||
|
self, "post", self.list_url, payload={"competency": 1}
|
||||||
|
)
|
||||||
|
|||||||
@ -4,6 +4,7 @@ from django.utils.decorators import method_decorator
|
|||||||
from rest_framework.parsers import JSONParser
|
from rest_framework.parsers import JSONParser
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from .models import (
|
from .models import (
|
||||||
Teacher,
|
Teacher,
|
||||||
Speciality,
|
Speciality,
|
||||||
@ -42,6 +43,8 @@ logger = logging.getLogger(__name__)
|
|||||||
@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')
|
||||||
class SpecialityListCreateView(APIView):
|
class SpecialityListCreateView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
establishment_id = request.GET.get('establishment_id', None)
|
establishment_id = request.GET.get('establishment_id', None)
|
||||||
if establishment_id is None:
|
if establishment_id is None:
|
||||||
@ -66,6 +69,8 @@ class SpecialityListCreateView(APIView):
|
|||||||
@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')
|
||||||
class SpecialityDetailView(APIView):
|
class SpecialityDetailView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request, id):
|
def get(self, request, id):
|
||||||
speciality = getObject(_objectName=Speciality, _columnName='id', _value=id)
|
speciality = getObject(_objectName=Speciality, _columnName='id', _value=id)
|
||||||
speciality_serializer=SpecialitySerializer(speciality)
|
speciality_serializer=SpecialitySerializer(speciality)
|
||||||
@ -87,6 +92,8 @@ class SpecialityDetailView(APIView):
|
|||||||
@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')
|
||||||
class TeacherListCreateView(APIView):
|
class TeacherListCreateView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
establishment_id = request.GET.get('establishment_id', None)
|
establishment_id = request.GET.get('establishment_id', None)
|
||||||
if establishment_id is None:
|
if establishment_id is None:
|
||||||
@ -121,6 +128,8 @@ class TeacherListCreateView(APIView):
|
|||||||
@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')
|
||||||
class TeacherDetailView(APIView):
|
class TeacherDetailView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get (self, request, id):
|
def get (self, request, id):
|
||||||
teacher = getObject(_objectName=Teacher, _columnName='id', _value=id)
|
teacher = getObject(_objectName=Teacher, _columnName='id', _value=id)
|
||||||
teacher_serializer=TeacherSerializer(teacher)
|
teacher_serializer=TeacherSerializer(teacher)
|
||||||
@ -169,6 +178,8 @@ class TeacherDetailView(APIView):
|
|||||||
@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')
|
||||||
class SchoolClassListCreateView(APIView):
|
class SchoolClassListCreateView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
establishment_id = request.GET.get('establishment_id', None)
|
establishment_id = request.GET.get('establishment_id', None)
|
||||||
if establishment_id is None:
|
if establishment_id is None:
|
||||||
@ -193,6 +204,8 @@ class SchoolClassListCreateView(APIView):
|
|||||||
@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')
|
||||||
class SchoolClassDetailView(APIView):
|
class SchoolClassDetailView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get (self, request, id):
|
def get (self, request, id):
|
||||||
schoolClass = getObject(_objectName=SchoolClass, _columnName='id', _value=id)
|
schoolClass = getObject(_objectName=SchoolClass, _columnName='id', _value=id)
|
||||||
classe_serializer=SchoolClassSerializer(schoolClass)
|
classe_serializer=SchoolClassSerializer(schoolClass)
|
||||||
@ -215,6 +228,8 @@ class SchoolClassDetailView(APIView):
|
|||||||
@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')
|
||||||
class PlanningListCreateView(APIView):
|
class PlanningListCreateView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
schedulesList=getAllObjects(Planning)
|
schedulesList=getAllObjects(Planning)
|
||||||
schedules_serializer=PlanningSerializer(schedulesList, many=True)
|
schedules_serializer=PlanningSerializer(schedulesList, many=True)
|
||||||
@ -233,6 +248,8 @@ class PlanningListCreateView(APIView):
|
|||||||
@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')
|
||||||
class PlanningDetailView(APIView):
|
class PlanningDetailView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get (self, request, id):
|
def get (self, request, id):
|
||||||
planning = getObject(_objectName=Planning, _columnName='classe_id', _value=id)
|
planning = getObject(_objectName=Planning, _columnName='classe_id', _value=id)
|
||||||
planning_serializer=PlanningSerializer(planning)
|
planning_serializer=PlanningSerializer(planning)
|
||||||
@ -263,6 +280,8 @@ class PlanningDetailView(APIView):
|
|||||||
@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')
|
||||||
class FeeListCreateView(APIView):
|
class FeeListCreateView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
establishment_id = request.GET.get('establishment_id', None)
|
establishment_id = request.GET.get('establishment_id', None)
|
||||||
if establishment_id is None:
|
if establishment_id is None:
|
||||||
@ -287,6 +306,8 @@ class FeeListCreateView(APIView):
|
|||||||
@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')
|
||||||
class FeeDetailView(APIView):
|
class FeeDetailView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request, id):
|
def get(self, request, id):
|
||||||
try:
|
try:
|
||||||
fee = Fee.objects.get(id=id)
|
fee = Fee.objects.get(id=id)
|
||||||
@ -313,6 +334,8 @@ class FeeDetailView(APIView):
|
|||||||
@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')
|
||||||
class DiscountListCreateView(APIView):
|
class DiscountListCreateView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
establishment_id = request.GET.get('establishment_id', None)
|
establishment_id = request.GET.get('establishment_id', None)
|
||||||
if establishment_id is None:
|
if establishment_id is None:
|
||||||
@ -337,6 +360,8 @@ class DiscountListCreateView(APIView):
|
|||||||
@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')
|
||||||
class DiscountDetailView(APIView):
|
class DiscountDetailView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request, id):
|
def get(self, request, id):
|
||||||
try:
|
try:
|
||||||
discount = Discount.objects.get(id=id)
|
discount = Discount.objects.get(id=id)
|
||||||
@ -363,6 +388,8 @@ class DiscountDetailView(APIView):
|
|||||||
@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')
|
||||||
class PaymentPlanListCreateView(APIView):
|
class PaymentPlanListCreateView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
establishment_id = request.GET.get('establishment_id', None)
|
establishment_id = request.GET.get('establishment_id', None)
|
||||||
if establishment_id is None:
|
if establishment_id is None:
|
||||||
@ -387,6 +414,8 @@ class PaymentPlanListCreateView(APIView):
|
|||||||
@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')
|
||||||
class PaymentPlanDetailView(APIView):
|
class PaymentPlanDetailView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request, id):
|
def get(self, request, id):
|
||||||
try:
|
try:
|
||||||
payment_plan = PaymentPlan.objects.get(id=id)
|
payment_plan = PaymentPlan.objects.get(id=id)
|
||||||
@ -413,6 +442,8 @@ class PaymentPlanDetailView(APIView):
|
|||||||
@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')
|
||||||
class PaymentModeListCreateView(APIView):
|
class PaymentModeListCreateView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
establishment_id = request.GET.get('establishment_id', None)
|
establishment_id = request.GET.get('establishment_id', None)
|
||||||
if establishment_id is None:
|
if establishment_id is None:
|
||||||
@ -437,6 +468,8 @@ class PaymentModeListCreateView(APIView):
|
|||||||
@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')
|
||||||
class PaymentModeDetailView(APIView):
|
class PaymentModeDetailView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request, id):
|
def get(self, request, id):
|
||||||
try:
|
try:
|
||||||
payment_mode = PaymentMode.objects.get(id=id)
|
payment_mode = PaymentMode.objects.get(id=id)
|
||||||
@ -463,6 +496,8 @@ class PaymentModeDetailView(APIView):
|
|||||||
@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')
|
||||||
class CompetencyListCreateView(APIView):
|
class CompetencyListCreateView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
cycle = request.GET.get('cycle')
|
cycle = request.GET.get('cycle')
|
||||||
if cycle is None:
|
if cycle is None:
|
||||||
@ -486,6 +521,8 @@ class CompetencyListCreateView(APIView):
|
|||||||
@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')
|
||||||
class CompetencyDetailView(APIView):
|
class CompetencyDetailView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request, id):
|
def get(self, request, id):
|
||||||
try:
|
try:
|
||||||
competency = Competency.objects.get(id=id)
|
competency = Competency.objects.get(id=id)
|
||||||
@ -517,6 +554,8 @@ class CompetencyDetailView(APIView):
|
|||||||
@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')
|
||||||
class EstablishmentCompetencyListCreateView(APIView):
|
class EstablishmentCompetencyListCreateView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
establishment_id = request.GET.get('establishment_id')
|
establishment_id = request.GET.get('establishment_id')
|
||||||
cycle = request.GET.get('cycle')
|
cycle = request.GET.get('cycle')
|
||||||
@ -710,6 +749,8 @@ class EstablishmentCompetencyListCreateView(APIView):
|
|||||||
@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')
|
||||||
class EstablishmentCompetencyDetailView(APIView):
|
class EstablishmentCompetencyDetailView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request, id):
|
def get(self, request, id):
|
||||||
try:
|
try:
|
||||||
ec = EstablishmentCompetency.objects.get(id=id)
|
ec = EstablishmentCompetency.objects.get(id=id)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.1.3 on 2025-11-30 11:02
|
# Generated by Django 5.1.3 on 2026-03-14 13:23
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|||||||
@ -2,6 +2,9 @@ from rest_framework import serializers
|
|||||||
from .models import SMTPSettings
|
from .models import SMTPSettings
|
||||||
|
|
||||||
class SMTPSettingsSerializer(serializers.ModelSerializer):
|
class SMTPSettingsSerializer(serializers.ModelSerializer):
|
||||||
|
# Le mot de passe SMTP est en écriture seule : il ne revient jamais dans les réponses API
|
||||||
|
smtp_password = serializers.CharField(write_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SMTPSettings
|
model = SMTPSettings
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
116
Back-End/Settings/tests_security.py
Normal file
116
Back-End/Settings/tests_security.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
"""
|
||||||
|
Tests de sécurité — Settings (SMTP)
|
||||||
|
Vérifie :
|
||||||
|
- Le mot de passe SMTP est absent des réponses GET (write_only)
|
||||||
|
- Authentification requise
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.test import TestCase, override_settings
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
|
||||||
|
from Auth.models import Profile, ProfileRole
|
||||||
|
from Establishment.models import Establishment
|
||||||
|
from Settings.models import SMTPSettings
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def create_user_with_role(email):
|
||||||
|
user = Profile.objects.create_user(
|
||||||
|
username=email, email=email, password="TestPass!123"
|
||||||
|
)
|
||||||
|
est = Establishment.objects.create(
|
||||||
|
name=f"Ecole {email}", address="1 rue Test",
|
||||||
|
total_capacity=50, establishment_type=[1]
|
||||||
|
)
|
||||||
|
ProfileRole.objects.create(
|
||||||
|
profile=user, role_type=ProfileRole.RoleType.PROFIL_ADMIN,
|
||||||
|
establishment=est, is_active=True
|
||||||
|
)
|
||||||
|
return user, est
|
||||||
|
|
||||||
|
|
||||||
|
OVERRIDE = dict(
|
||||||
|
CACHES={'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}},
|
||||||
|
SESSION_ENGINE='django.contrib.sessions.backends.db',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@override_settings(**OVERRIDE)
|
||||||
|
class SMTPSettingsAuthTest(TestCase):
|
||||||
|
"""Authentification requise sur l'endpoint SMTP."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.url = reverse('Settings:smtp_settings')
|
||||||
|
|
||||||
|
def test_sans_auth_retourne_401(self):
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(**OVERRIDE)
|
||||||
|
class SMTPPasswordNotExposedTest(TestCase):
|
||||||
|
"""
|
||||||
|
Le mot de passe SMTP ne doit jamais apparaître dans les réponses GET.
|
||||||
|
Avant la correction, smtp_password était retourné en clair à tout
|
||||||
|
utilisateur authentifié (incluant les parents).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.url = reverse('Settings:smtp_settings')
|
||||||
|
self.user, self.est = create_user_with_role('smtp_test@test.com')
|
||||||
|
SMTPSettings.objects.create(
|
||||||
|
establishment=self.est,
|
||||||
|
smtp_server='smtp.example.com',
|
||||||
|
smtp_port=587,
|
||||||
|
smtp_user='user@example.com',
|
||||||
|
smtp_password='super_secret_password_123',
|
||||||
|
use_tls=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_smtp_password_absent_de_la_reponse(self):
|
||||||
|
"""
|
||||||
|
GET /settings/smtp/ ne doit pas retourner smtp_password.
|
||||||
|
"""
|
||||||
|
token = str(RefreshToken.for_user(self.user).access_token)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')
|
||||||
|
response = self.client.get(self.url, {'establishment_id': self.est.id})
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
# Le mot de passe ne doit pas être dans la réponse (write_only)
|
||||||
|
self.assertNotIn(
|
||||||
|
'smtp_password', data,
|
||||||
|
"smtp_password ne doit pas être exposé dans les réponses API (OWASP A02 - Cryptographic Failures)"
|
||||||
|
)
|
||||||
|
# Vérification supplémentaire : la valeur secrète n'est pas dans la réponse brute
|
||||||
|
self.assertNotIn('super_secret_password_123', response.content.decode())
|
||||||
|
|
||||||
|
def test_smtp_password_accepte_en_ecriture(self):
|
||||||
|
"""
|
||||||
|
POST /settings/smtp/ doit accepter smtp_password (write_only ne bloque pas l'écriture).
|
||||||
|
"""
|
||||||
|
token = str(RefreshToken.for_user(self.user).access_token)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')
|
||||||
|
payload = {
|
||||||
|
'establishment': self.est.id,
|
||||||
|
'smtp_server': 'smtp.newserver.com',
|
||||||
|
'smtp_port': 465,
|
||||||
|
'smtp_user': 'new@example.com',
|
||||||
|
'smtp_password': 'nouveau_mot_de_passe',
|
||||||
|
'use_tls': False,
|
||||||
|
'use_ssl': True,
|
||||||
|
}
|
||||||
|
from rest_framework.test import APIRequestFactory
|
||||||
|
response = self.client.post(self.url, data=payload, format='json')
|
||||||
|
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_201_CREATED])
|
||||||
@ -5,8 +5,10 @@ from .serializers import SMTPSettingsSerializer
|
|||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
|
||||||
class SMTPSettingsView(APIView):
|
class SMTPSettingsView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
"""
|
"""
|
||||||
API pour gérer les paramètres SMTP.
|
API pour gérer les paramètres SMTP.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.1.3 on 2025-11-30 11:02
|
# Generated by Django 5.1.3 on 2026-03-14 13:23
|
||||||
|
|
||||||
import Subscriptions.models
|
import Subscriptions.models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
@ -51,6 +51,7 @@ class Migration(migrations.Migration):
|
|||||||
('name', models.CharField(default='', max_length=255)),
|
('name', models.CharField(default='', max_length=255)),
|
||||||
('file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_school_file_upload_to)),
|
('file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_school_file_upload_to)),
|
||||||
('formTemplateData', models.JSONField(blank=True, default=list, null=True)),
|
('formTemplateData', models.JSONField(blank=True, default=list, null=True)),
|
||||||
|
('isValidated', models.BooleanField(default=False)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
@ -93,9 +94,9 @@ class Migration(migrations.Migration):
|
|||||||
('school_year', models.CharField(blank=True, default='', max_length=9)),
|
('school_year', models.CharField(blank=True, default='', max_length=9)),
|
||||||
('notes', models.CharField(blank=True, max_length=200)),
|
('notes', models.CharField(blank=True, max_length=200)),
|
||||||
('registration_link_code', models.CharField(blank=True, default='', max_length=200)),
|
('registration_link_code', models.CharField(blank=True, default='', max_length=200)),
|
||||||
('registration_file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_file_path)),
|
('registration_file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_form_file_upload_to)),
|
||||||
('sepa_file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_file_path)),
|
('sepa_file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_form_file_upload_to)),
|
||||||
('fusion_file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_file_path)),
|
('fusion_file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_form_file_upload_to)),
|
||||||
('associated_rf', models.CharField(blank=True, default='', max_length=200)),
|
('associated_rf', models.CharField(blank=True, default='', max_length=200)),
|
||||||
('discounts', models.ManyToManyField(blank=True, related_name='register_forms', to='School.discount')),
|
('discounts', models.ManyToManyField(blank=True, related_name='register_forms', to='School.discount')),
|
||||||
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='register_forms', to='Establishment.establishment')),
|
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='register_forms', to='Establishment.establishment')),
|
||||||
@ -166,6 +167,8 @@ class Migration(migrations.Migration):
|
|||||||
('name', models.CharField(default='', max_length=255)),
|
('name', models.CharField(default='', max_length=255)),
|
||||||
('is_required', models.BooleanField(default=False)),
|
('is_required', models.BooleanField(default=False)),
|
||||||
('formMasterData', models.JSONField(blank=True, default=list, null=True)),
|
('formMasterData', models.JSONField(blank=True, default=list, null=True)),
|
||||||
|
('file', models.FileField(blank=True, help_text='Fichier du formulaire existant (PDF, DOC, etc.)', null=True, upload_to=Subscriptions.models.registration_school_file_master_upload_to)),
|
||||||
|
('establishment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='school_file_masters', to='Establishment.establishment')),
|
||||||
('groups', models.ManyToManyField(blank=True, related_name='school_file_masters', to='Subscriptions.registrationfilegroup')),
|
('groups', models.ManyToManyField(blank=True, related_name='school_file_masters', to='Subscriptions.registrationfilegroup')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -194,6 +197,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_parent_file_upload_to)),
|
('file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_parent_file_upload_to)),
|
||||||
|
('isValidated', models.BooleanField(default=False)),
|
||||||
('master', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_file_templates', to='Subscriptions.registrationparentfilemaster')),
|
('master', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_file_templates', to='Subscriptions.registrationparentfilemaster')),
|
||||||
('registration_form', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_file_templates', to='Subscriptions.registrationform')),
|
('registration_form', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_file_templates', to='Subscriptions.registrationform')),
|
||||||
],
|
],
|
||||||
|
|||||||
Binary file not shown.
2
Back-End/runTests.sh
Executable file
2
Back-End/runTests.sh
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
docker exec -it n3wt-school-backend-1 python3 manage.py test --settings=N3wtSchool.test_settings Auth.tests
|
||||||
@ -66,7 +66,7 @@ if __name__ == "__main__":
|
|||||||
if run_command(command) != 0:
|
if run_command(command) != 0:
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
if migrate_data:
|
|
||||||
for command in migrate_commands:
|
for command in migrate_commands:
|
||||||
if run_command(command) != 0:
|
if run_command(command) != 0:
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|||||||
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": ["next/babel"],
|
|
||||||
"plugins": []
|
|
||||||
}
|
|
||||||
@ -31,6 +31,7 @@ import { fetchRegistrationSchoolFileMasters } from '@/app/actions/registerFileGr
|
|||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
import { PlanningProvider, PlanningModes } from '@/context/PlanningContext';
|
import { PlanningProvider, PlanningModes } from '@/context/PlanningContext';
|
||||||
|
import { updatePlanning } from '@/app/actions/planningAction';
|
||||||
import CompetenciesList from '@/components/Structure/Competencies/CompetenciesList';
|
import CompetenciesList from '@/components/Structure/Competencies/CompetenciesList';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
@ -259,20 +260,10 @@ export default function Page() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdatePlanning = (url, planningId, updatedData) => {
|
const handleUpdatePlanning = (planningId, updatedData) => {
|
||||||
fetch(`${url}/${planningId}`, {
|
updatePlanning(planningId, updatedData, csrfToken)
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(updatedData),
|
|
||||||
credentials: 'include',
|
|
||||||
})
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
logger.debug('Planning mis à jour avec succès :', data);
|
logger.debug('Planning mis à jour avec succès :', data);
|
||||||
//setDatas(data);
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logger.error('Erreur :', error);
|
logger.error('Erreur :', error);
|
||||||
|
|||||||
@ -1,4 +1,15 @@
|
|||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
|
import { signOut } from 'next-auth/react';
|
||||||
|
|
||||||
|
let isSigningOut = false;
|
||||||
|
|
||||||
|
export const triggerSignOut = async () => {
|
||||||
|
if (isSigningOut || typeof window === 'undefined') return;
|
||||||
|
isSigningOut = true;
|
||||||
|
logger.warn('Session expirée, déconnexion en cours...');
|
||||||
|
await signOut({ callbackUrl: '/users/login' });
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {*} response
|
* @param {*} response
|
||||||
@ -6,6 +17,18 @@ import logger from '@/utils/logger';
|
|||||||
*/
|
*/
|
||||||
export const requestResponseHandler = async (response) => {
|
export const requestResponseHandler = async (response) => {
|
||||||
try {
|
try {
|
||||||
|
if (response.status === 401) {
|
||||||
|
// On lève une erreur plutôt que de déclencher un signOut automatique.
|
||||||
|
// Plusieurs requêtes concurrent pourraient déclencher des signOut en cascade.
|
||||||
|
// Le signOut est géré proprement via RefreshTokenError dans getAuthToken.
|
||||||
|
const body = await response.json().catch(() => ({}));
|
||||||
|
const error = new Error(
|
||||||
|
body?.detail || body?.errorMessage || 'Session expirée'
|
||||||
|
);
|
||||||
|
error.status = 401;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
const body = await response?.json();
|
const body = await response?.json();
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return body;
|
return body;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { signOut, signIn } from 'next-auth/react';
|
import { signOut, signIn } from 'next-auth/react';
|
||||||
import { errorHandler, requestResponseHandler } from './actionsHandlers';
|
import { errorHandler, requestResponseHandler } from './actionsHandlers';
|
||||||
|
import { fetchWithAuth } from '@/utils/fetchWithAuth';
|
||||||
import {
|
import {
|
||||||
BE_AUTH_LOGIN_URL,
|
BE_AUTH_LOGIN_URL,
|
||||||
BE_AUTH_REFRESH_JWT_URL,
|
BE_AUTH_REFRESH_JWT_URL,
|
||||||
@ -73,92 +74,49 @@ export const fetchProfileRoles = (
|
|||||||
if (page !== '' && pageSize !== '') {
|
if (page !== '' && pageSize !== '') {
|
||||||
url = `${BE_AUTH_PROFILES_ROLES_URL}?filter=${filter}&establishment_id=${establishment}&page=${page}`;
|
url = `${BE_AUTH_PROFILES_ROLES_URL}?filter=${filter}&establishment_id=${establishment}&page=${page}`;
|
||||||
}
|
}
|
||||||
return fetch(url, {
|
return fetchWithAuth(url);
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateProfileRoles = (id, data, csrfToken) => {
|
export const updateProfileRoles = (id, data, csrfToken) => {
|
||||||
const request = new Request(`${BE_AUTH_PROFILES_ROLES_URL}/${id}`, {
|
return fetchWithAuth(`${BE_AUTH_PROFILES_ROLES_URL}/${id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteProfileRoles = async (id, csrfToken) => {
|
export const deleteProfileRoles = (id, csrfToken) => {
|
||||||
const response = await fetch(`${BE_AUTH_PROFILES_ROLES_URL}/${id}`, {
|
return fetchWithAuth(`${BE_AUTH_PROFILES_ROLES_URL}/${id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
// Extraire le message d'erreur du backend
|
|
||||||
const errorData = await response.json();
|
|
||||||
const errorMessage =
|
|
||||||
errorData?.error ||
|
|
||||||
'Une erreur est survenue lors de la suppression du profil.';
|
|
||||||
|
|
||||||
// Jeter une erreur avec le message spécifique
|
|
||||||
throw new Error(errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchProfiles = () => {
|
export const fetchProfiles = () => {
|
||||||
return fetch(`${BE_AUTH_PROFILES_URL}`)
|
return fetchWithAuth(`${BE_AUTH_PROFILES_URL}`);
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createProfile = (data, csrfToken) => {
|
export const createProfile = (data, csrfToken) => {
|
||||||
const request = new Request(`${BE_AUTH_PROFILES_URL}`, {
|
return fetchWithAuth(`${BE_AUTH_PROFILES_URL}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteProfile = (id, csrfToken) => {
|
export const deleteProfile = (id, csrfToken) => {
|
||||||
const request = new Request(`${BE_AUTH_PROFILES_URL}/${id}`, {
|
return fetchWithAuth(`${BE_AUTH_PROFILES_URL}/${id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
});
|
||||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateProfile = (id, data, csrfToken) => {
|
export const updateProfile = (id, data, csrfToken) => {
|
||||||
const request = new Request(`${BE_AUTH_PROFILES_URL}/${id}`, {
|
return fetchWithAuth(`${BE_AUTH_PROFILES_URL}/${id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sendNewPassword = (data, csrfToken) => {
|
export const sendNewPassword = (data, csrfToken) => {
|
||||||
|
|||||||
@ -2,33 +2,20 @@ import {
|
|||||||
BE_GESTIONEMAIL_SEARCH_RECIPIENTS_URL,
|
BE_GESTIONEMAIL_SEARCH_RECIPIENTS_URL,
|
||||||
BE_GESTIONEMAIL_SEND_EMAIL_URL,
|
BE_GESTIONEMAIL_SEND_EMAIL_URL,
|
||||||
} from '@/utils/Url';
|
} from '@/utils/Url';
|
||||||
import { errorHandler, requestResponseHandler } from './actionsHandlers';
|
import { fetchWithAuth } from '@/utils/fetchWithAuth';
|
||||||
import { getCsrfToken } from '@/utils/getCsrfToken';
|
import { getCsrfToken } from '@/utils/getCsrfToken';
|
||||||
|
|
||||||
// Recherche de destinataires pour email
|
// Recherche de destinataires pour email
|
||||||
export const searchRecipients = (establishmentId, query) => {
|
export const searchRecipients = (establishmentId, query) => {
|
||||||
const url = `${BE_GESTIONEMAIL_SEARCH_RECIPIENTS_URL}/?establishment_id=${establishmentId}&q=${encodeURIComponent(query)}`;
|
const url = `${BE_GESTIONEMAIL_SEARCH_RECIPIENTS_URL}/?establishment_id=${establishmentId}&q=${encodeURIComponent(query)}`;
|
||||||
return fetch(url, {
|
return fetchWithAuth(url);
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
// Envoyer un email
|
// Envoyer un email
|
||||||
export const sendEmail = async (messageData) => {
|
export const sendEmail = async (messageData) => {
|
||||||
const csrfToken = getCsrfToken();
|
const csrfToken = getCsrfToken();
|
||||||
return fetch(BE_GESTIONEMAIL_SEND_EMAIL_URL, {
|
return fetchWithAuth(BE_GESTIONEMAIL_SEND_EMAIL_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
body: JSON.stringify(messageData),
|
body: JSON.stringify(messageData),
|
||||||
})
|
});
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,23 +7,9 @@ import {
|
|||||||
BE_GESTIONMESSAGERIE_UPLOAD_FILE_URL,
|
BE_GESTIONMESSAGERIE_UPLOAD_FILE_URL,
|
||||||
BE_GESTIONMESSAGERIE_DELETE_CONVERSATION_URL,
|
BE_GESTIONMESSAGERIE_DELETE_CONVERSATION_URL,
|
||||||
} from '@/utils/Url';
|
} from '@/utils/Url';
|
||||||
import { errorHandler, requestResponseHandler } from './actionsHandlers';
|
import { fetchWithAuth, getAuthToken } from '@/utils/fetchWithAuth';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
|
|
||||||
// Helper pour construire les en-têtes avec CSRF
|
|
||||||
const buildHeaders = (csrfToken) => {
|
|
||||||
const headers = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ajouter le token CSRF
|
|
||||||
if (csrfToken) {
|
|
||||||
headers['X-CSRFToken'] = csrfToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
return headers;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupère les conversations d'un utilisateur
|
* Récupère les conversations d'un utilisateur
|
||||||
*/
|
*/
|
||||||
@ -31,15 +17,12 @@ export const fetchConversations = async (userId, csrfToken) => {
|
|||||||
try {
|
try {
|
||||||
// Utiliser la nouvelle route avec user_id en paramètre d'URL
|
// Utiliser la nouvelle route avec user_id en paramètre d'URL
|
||||||
const url = `${BE_GESTIONMESSAGERIE_CONVERSATIONS_URL}/user/${userId}/`;
|
const url = `${BE_GESTIONMESSAGERIE_CONVERSATIONS_URL}/user/${userId}/`;
|
||||||
const response = await fetch(url, {
|
return await fetchWithAuth(url, {
|
||||||
method: 'GET',
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
headers: buildHeaders(csrfToken),
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
});
|
||||||
return await requestResponseHandler(response);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Erreur lors de la récupération des conversations:', error);
|
logger.error('Erreur lors de la récupération des conversations:', error);
|
||||||
return errorHandler(error);
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -62,15 +45,12 @@ export const fetchMessages = async (
|
|||||||
url += `&user_id=${userId}`;
|
url += `&user_id=${userId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(url, {
|
return await fetchWithAuth(url, {
|
||||||
method: 'GET',
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
headers: buildHeaders(csrfToken),
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
});
|
||||||
return await requestResponseHandler(response);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Erreur lors de la récupération des messages:', error);
|
logger.error('Erreur lors de la récupération des messages:', error);
|
||||||
return errorHandler(error);
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -79,16 +59,14 @@ export const fetchMessages = async (
|
|||||||
*/
|
*/
|
||||||
export const sendMessage = async (messageData, csrfToken) => {
|
export const sendMessage = async (messageData, csrfToken) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(BE_GESTIONMESSAGERIE_SEND_MESSAGE_URL, {
|
return await fetchWithAuth(BE_GESTIONMESSAGERIE_SEND_MESSAGE_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: buildHeaders(csrfToken),
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
credentials: 'include',
|
|
||||||
body: JSON.stringify(messageData),
|
body: JSON.stringify(messageData),
|
||||||
});
|
});
|
||||||
return await requestResponseHandler(response);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Erreur lors de l'envoi du message:", error);
|
logger.error("Erreur lors de l'envoi du message:", error);
|
||||||
return errorHandler(error);
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -103,17 +81,14 @@ export const createConversation = async (participantIds, csrfToken) => {
|
|||||||
name: '', // Le nom sera généré côté backend
|
name: '', // Le nom sera généré côté backend
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await fetch(BE_GESTIONMESSAGERIE_CREATE_CONVERSATION_URL, {
|
return await fetchWithAuth(BE_GESTIONMESSAGERIE_CREATE_CONVERSATION_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: buildHeaders(csrfToken),
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
credentials: 'include',
|
|
||||||
body: JSON.stringify(requestBody),
|
body: JSON.stringify(requestBody),
|
||||||
});
|
});
|
||||||
|
|
||||||
return await requestResponseHandler(response);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Erreur lors de la création de la conversation:', error);
|
logger.error('Erreur lors de la création de la conversation:', error);
|
||||||
return errorHandler(error);
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -132,16 +107,12 @@ export const searchMessagerieRecipients = async (
|
|||||||
|
|
||||||
const url = `${baseUrl}?establishment_id=${establishmentId}&q=${encodeURIComponent(query)}`;
|
const url = `${baseUrl}?establishment_id=${establishmentId}&q=${encodeURIComponent(query)}`;
|
||||||
|
|
||||||
const response = await fetch(url, {
|
return await fetchWithAuth(url, {
|
||||||
method: 'GET',
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
headers: buildHeaders(csrfToken),
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return await requestResponseHandler(response);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Erreur lors de la recherche des destinataires:', error);
|
logger.error('Erreur lors de la recherche des destinataires:', error);
|
||||||
return errorHandler(error);
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -150,19 +121,17 @@ export const searchMessagerieRecipients = async (
|
|||||||
*/
|
*/
|
||||||
export const markAsRead = async (conversationId, userId, csrfToken) => {
|
export const markAsRead = async (conversationId, userId, csrfToken) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(BE_GESTIONMESSAGERIE_MARK_AS_READ_URL, {
|
return await fetchWithAuth(BE_GESTIONMESSAGERIE_MARK_AS_READ_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: buildHeaders(csrfToken),
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
credentials: 'include',
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
conversation_id: conversationId,
|
conversation_id: conversationId,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
return await requestResponseHandler(response);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Erreur lors du marquage des messages comme lus:', error);
|
logger.error('Erreur lors du marquage des messages comme lus:', error);
|
||||||
return errorHandler(error);
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -181,6 +150,7 @@ export const uploadFile = async (
|
|||||||
formData.append('conversation_id', conversationId);
|
formData.append('conversation_id', conversationId);
|
||||||
formData.append('sender_id', senderId);
|
formData.append('sender_id', senderId);
|
||||||
|
|
||||||
|
const token = await getAuthToken();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
@ -223,7 +193,10 @@ export const uploadFile = async (
|
|||||||
xhr.withCredentials = true;
|
xhr.withCredentials = true;
|
||||||
xhr.timeout = 30000;
|
xhr.timeout = 30000;
|
||||||
|
|
||||||
// Ajouter le header CSRF pour XMLHttpRequest
|
// Ajouter les headers d'authentification pour XMLHttpRequest
|
||||||
|
if (token) {
|
||||||
|
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
|
||||||
|
}
|
||||||
if (csrfToken) {
|
if (csrfToken) {
|
||||||
xhr.setRequestHeader('X-CSRFToken', csrfToken);
|
xhr.setRequestHeader('X-CSRFToken', csrfToken);
|
||||||
}
|
}
|
||||||
@ -238,14 +211,12 @@ export const uploadFile = async (
|
|||||||
export const deleteConversation = async (conversationId, csrfToken) => {
|
export const deleteConversation = async (conversationId, csrfToken) => {
|
||||||
try {
|
try {
|
||||||
const url = `${BE_GESTIONMESSAGERIE_DELETE_CONVERSATION_URL}/${conversationId}/`;
|
const url = `${BE_GESTIONMESSAGERIE_DELETE_CONVERSATION_URL}/${conversationId}/`;
|
||||||
const response = await fetch(url, {
|
return await fetchWithAuth(url, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: buildHeaders(csrfToken),
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
credentials: 'include',
|
|
||||||
});
|
});
|
||||||
return await requestResponseHandler(response);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Erreur lors de la suppression de la conversation:', error);
|
logger.error('Erreur lors de la suppression de la conversation:', error);
|
||||||
return errorHandler(error);
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,49 +1,31 @@
|
|||||||
import { BE_PLANNING_PLANNINGS_URL, BE_PLANNING_EVENTS_URL } from '@/utils/Url';
|
import { BE_PLANNING_PLANNINGS_URL, BE_PLANNING_EVENTS_URL } from '@/utils/Url';
|
||||||
import { errorHandler, requestResponseHandler } from './actionsHandlers';
|
import { fetchWithAuth } from '@/utils/fetchWithAuth';
|
||||||
|
|
||||||
const getData = (url) => {
|
const getData = (url) => {
|
||||||
return fetch(`${url}`).then(requestResponseHandler).catch(errorHandler);
|
return fetchWithAuth(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createDatas = (url, newData, csrfToken) => {
|
const createDatas = (url, newData, csrfToken) => {
|
||||||
return fetch(url, {
|
return fetchWithAuth(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(newData),
|
body: JSON.stringify(newData),
|
||||||
credentials: 'include',
|
});
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateDatas = (url, updatedData, csrfToken) => {
|
const updateDatas = (url, updatedData, csrfToken) => {
|
||||||
return fetch(`${url}`, {
|
return fetchWithAuth(url, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(updatedData),
|
body: JSON.stringify(updatedData),
|
||||||
credentials: 'include',
|
});
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeDatas = (url, csrfToken) => {
|
const removeDatas = (url, csrfToken) => {
|
||||||
return fetch(`${url}`, {
|
return fetchWithAuth(url, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'Content-Type': 'application/json',
|
});
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchPlannings = (
|
export const fetchPlannings = (
|
||||||
|
|||||||
@ -5,213 +5,113 @@ import {
|
|||||||
BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL,
|
BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL,
|
||||||
BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL
|
BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL
|
||||||
} from '@/utils/Url';
|
} from '@/utils/Url';
|
||||||
import { errorHandler, requestResponseHandler } from './actionsHandlers';
|
import { fetchWithAuth, fetchWithAuthRaw } from '@/utils/fetchWithAuth';
|
||||||
|
|
||||||
// FETCH requests
|
// FETCH requests
|
||||||
|
|
||||||
export async function fetchRegistrationFileGroups(establishment) {
|
export async function fetchRegistrationFileGroups(establishment) {
|
||||||
const response = await fetch(
|
return fetchWithAuth(
|
||||||
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}?establishment_id=${establishment}`,
|
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}?establishment_id=${establishment}`
|
||||||
{
|
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to fetch file groups');
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchRegistrationFileFromGroup = async (groupId) => {
|
export const fetchRegistrationFileFromGroup = (groupId) => {
|
||||||
const response = await fetch(
|
return fetchWithAuth(
|
||||||
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}/school_file_templates`,
|
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}/school_file_templates`
|
||||||
{
|
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(
|
|
||||||
'Erreur lors de la récupération des fichiers associés au groupe'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchRegistrationSchoolFileMasters = (establishment) => {
|
export const fetchRegistrationSchoolFileMasters = (establishment) => {
|
||||||
let url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}?establishment_id=${establishment}`;
|
const url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}?establishment_id=${establishment}`;
|
||||||
const request = new Request(`${url}`, {
|
return fetchWithAuth(url);
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchRegistrationParentFileMasters = (establishment) => {
|
export const fetchRegistrationParentFileMasters = (establishment) => {
|
||||||
let url = `${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}?establishment_id=${establishment}`;
|
const url = `${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}?establishment_id=${establishment}`;
|
||||||
const request = new Request(`${url}`, {
|
return fetchWithAuth(url);
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchRegistrationSchoolFileTemplates = (establishment) => {
|
export const fetchRegistrationSchoolFileTemplates = (establishment) => {
|
||||||
let url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}?establishment_id=${establishment}`;
|
const url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}?establishment_id=${establishment}`;
|
||||||
const request = new Request(`${url}`, {
|
return fetchWithAuth(url);
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// CREATE requests
|
// CREATE requests
|
||||||
|
|
||||||
export async function createRegistrationFileGroup(groupData, csrfToken) {
|
export async function createRegistrationFileGroup(groupData, csrfToken) {
|
||||||
const response = await fetch(
|
return fetchWithAuth(`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}`, {
|
||||||
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(groupData),
|
body: JSON.stringify(groupData),
|
||||||
credentials: 'include',
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to create file group');
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createRegistrationSchoolFileMaster = (data, csrfToken) => {
|
export const createRegistrationSchoolFileMaster = (data, csrfToken) => {
|
||||||
// Toujours FormData, jamais JSON
|
// Toujours FormData, jamais JSON
|
||||||
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}`, {
|
return fetchWithAuth(`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
body: data,
|
body: data,
|
||||||
headers: {
|
});
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createRegistrationParentFileMaster = (data, csrfToken) => {
|
export const createRegistrationParentFileMaster = (data, csrfToken) => {
|
||||||
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}`, {
|
return fetchWithAuth(`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
headers: {
|
});
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createRegistrationSchoolFileTemplate = (data, csrfToken) => {
|
export const createRegistrationSchoolFileTemplate = (data, csrfToken) => {
|
||||||
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}`, {
|
return fetchWithAuth(`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
headers: {
|
});
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createRegistrationParentFileTemplate = (data, csrfToken) => {
|
export const createRegistrationParentFileTemplate = (data, csrfToken) => {
|
||||||
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL}`, {
|
return fetchWithAuth(`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
headers: {
|
});
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// EDIT requests
|
// EDIT requests
|
||||||
|
|
||||||
export const editRegistrationFileGroup = async (
|
export const editRegistrationFileGroup = (groupId, groupData, csrfToken) => {
|
||||||
groupId,
|
return fetchWithAuth(
|
||||||
groupData,
|
|
||||||
csrfToken
|
|
||||||
) => {
|
|
||||||
const response = await fetch(
|
|
||||||
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}`,
|
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(groupData),
|
body: JSON.stringify(groupData),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Erreur lors de la modification du groupe');
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const editRegistrationSchoolFileMaster = (fileId, data, csrfToken) => {
|
export const editRegistrationSchoolFileMaster = (fileId, data, csrfToken) => {
|
||||||
return fetch(
|
return fetchWithAuth(
|
||||||
`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}/${fileId}`,
|
`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}/${fileId}`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
body: data,
|
body: data,
|
||||||
headers: {
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const editRegistrationParentFileMaster = (id, data, csrfToken) => {
|
export const editRegistrationParentFileMaster = (id, data, csrfToken) => {
|
||||||
return fetch(
|
return fetchWithAuth(
|
||||||
`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}/${id}`,
|
`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}/${id}`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
headers: {
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const editRegistrationSchoolFileTemplates = (
|
export const editRegistrationSchoolFileTemplates = (
|
||||||
@ -219,19 +119,14 @@ export const editRegistrationSchoolFileTemplates = (
|
|||||||
data,
|
data,
|
||||||
csrfToken
|
csrfToken
|
||||||
) => {
|
) => {
|
||||||
return fetch(
|
return fetchWithAuth(
|
||||||
`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}/${fileId}`,
|
`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}/${fileId}`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
body: data,
|
body: data,
|
||||||
headers: {
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const editRegistrationParentFileTemplates = (
|
export const editRegistrationParentFileTemplates = (
|
||||||
@ -239,86 +134,64 @@ export const editRegistrationParentFileTemplates = (
|
|||||||
data,
|
data,
|
||||||
csrfToken
|
csrfToken
|
||||||
) => {
|
) => {
|
||||||
return fetch(
|
return fetchWithAuth(
|
||||||
`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL}/${fileId}`,
|
`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL}/${fileId}`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
body: data,
|
body: data,
|
||||||
headers: {
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// DELETE requests
|
// DELETE requests
|
||||||
|
|
||||||
export async function deleteRegistrationFileGroup(groupId, csrfToken) {
|
export async function deleteRegistrationFileGroup(groupId, csrfToken) {
|
||||||
const response = await fetch(
|
return fetchWithAuthRaw(
|
||||||
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}`,
|
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}`,
|
||||||
{
|
{
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteRegistrationSchoolFileMaster = (fileId, csrfToken) => {
|
export const deleteRegistrationSchoolFileMaster = (fileId, csrfToken) => {
|
||||||
return fetch(
|
return fetchWithAuthRaw(
|
||||||
`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}/${fileId}`,
|
`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}/${fileId}`,
|
||||||
{
|
{
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteRegistrationParentFileMaster = (id, csrfToken) => {
|
export const deleteRegistrationParentFileMaster = (id, csrfToken) => {
|
||||||
return fetch(
|
return fetchWithAuthRaw(
|
||||||
`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}/${id}`,
|
`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}/${id}`,
|
||||||
{
|
{
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteRegistrationSchoolFileTemplates = (fileId, csrfToken) => {
|
export const deleteRegistrationSchoolFileTemplates = (fileId, csrfToken) => {
|
||||||
return fetch(
|
return fetchWithAuthRaw(
|
||||||
`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}/${fileId}`,
|
`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}/${fileId}`,
|
||||||
{
|
{
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteRegistrationParentFileTemplate = (id, csrfToken) => {
|
export const deleteRegistrationParentFileTemplate = (id, csrfToken) => {
|
||||||
return fetch(
|
return fetchWithAuthRaw(
|
||||||
`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL}/${id}`,
|
`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL}/${id}`,
|
||||||
{
|
{
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,185 +10,125 @@ import {
|
|||||||
BE_SCHOOL_ESTABLISHMENT_URL,
|
BE_SCHOOL_ESTABLISHMENT_URL,
|
||||||
BE_SCHOOL_ESTABLISHMENT_COMPETENCIES_URL,
|
BE_SCHOOL_ESTABLISHMENT_COMPETENCIES_URL,
|
||||||
} from '@/utils/Url';
|
} from '@/utils/Url';
|
||||||
import { errorHandler, requestResponseHandler } from './actionsHandlers';
|
import { fetchWithAuth } from '@/utils/fetchWithAuth';
|
||||||
|
|
||||||
export const deleteEstablishmentCompetencies = (ids, csrfToken) => {
|
export const deleteEstablishmentCompetencies = (ids, csrfToken) => {
|
||||||
return fetch(BE_SCHOOL_ESTABLISHMENT_COMPETENCIES_URL, {
|
return fetchWithAuth(BE_SCHOOL_ESTABLISHMENT_COMPETENCIES_URL, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ ids }),
|
body: JSON.stringify({ ids }),
|
||||||
credentials: 'include',
|
});
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createEstablishmentCompetencies = (newData, csrfToken) => {
|
export const createEstablishmentCompetencies = (newData, csrfToken) => {
|
||||||
return fetch(BE_SCHOOL_ESTABLISHMENT_COMPETENCIES_URL, {
|
return fetchWithAuth(BE_SCHOOL_ESTABLISHMENT_COMPETENCIES_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(newData),
|
body: JSON.stringify(newData),
|
||||||
credentials: 'include',
|
});
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchEstablishmentCompetencies = (establishment, cycle = 1) => {
|
export const fetchEstablishmentCompetencies = (establishment, cycle = 1) => {
|
||||||
return fetch(
|
return fetchWithAuth(
|
||||||
`${BE_SCHOOL_ESTABLISHMENT_COMPETENCIES_URL}?establishment_id=${establishment}&cycle=${cycle}`
|
`${BE_SCHOOL_ESTABLISHMENT_COMPETENCIES_URL}?establishment_id=${establishment}&cycle=${cycle}`
|
||||||
)
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchSpecialities = (establishment) => {
|
|
||||||
return fetch(
|
|
||||||
`${BE_SCHOOL_SPECIALITIES_URL}?establishment_id=${establishment}`
|
|
||||||
)
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchTeachers = (establishment) => {
|
|
||||||
return fetch(`${BE_SCHOOL_TEACHERS_URL}?establishment_id=${establishment}`)
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchClasses = (establishment) => {
|
|
||||||
return fetch(
|
|
||||||
`${BE_SCHOOL_SCHOOLCLASSES_URL}?establishment_id=${establishment}`
|
|
||||||
)
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchClasse = (id) => {
|
|
||||||
return fetch(`${BE_SCHOOL_SCHOOLCLASSES_URL}/${id}`).then(
|
|
||||||
requestResponseHandler
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fetchSpecialities = (establishment) => {
|
||||||
|
return fetchWithAuth(
|
||||||
|
`${BE_SCHOOL_SPECIALITIES_URL}?establishment_id=${establishment}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchTeachers = (establishment) => {
|
||||||
|
return fetchWithAuth(`${BE_SCHOOL_TEACHERS_URL}?establishment_id=${establishment}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchClasses = (establishment) => {
|
||||||
|
return fetchWithAuth(
|
||||||
|
`${BE_SCHOOL_SCHOOLCLASSES_URL}?establishment_id=${establishment}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchClasse = (id) => {
|
||||||
|
return fetchWithAuth(`${BE_SCHOOL_SCHOOLCLASSES_URL}/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
export const fetchSchedules = () => {
|
export const fetchSchedules = () => {
|
||||||
return fetch(`${BE_SCHOOL_PLANNINGS_URL}`)
|
return fetchWithAuth(`${BE_SCHOOL_PLANNINGS_URL}`);
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchRegistrationDiscounts = (establishment) => {
|
export const fetchRegistrationDiscounts = (establishment) => {
|
||||||
return fetch(
|
return fetchWithAuth(
|
||||||
`${BE_SCHOOL_DISCOUNTS_URL}?filter=registration&establishment_id=${establishment}`
|
`${BE_SCHOOL_DISCOUNTS_URL}?filter=registration&establishment_id=${establishment}`
|
||||||
)
|
);
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchTuitionDiscounts = (establishment) => {
|
export const fetchTuitionDiscounts = (establishment) => {
|
||||||
return fetch(
|
return fetchWithAuth(
|
||||||
`${BE_SCHOOL_DISCOUNTS_URL}?filter=tuition&establishment_id=${establishment}`
|
`${BE_SCHOOL_DISCOUNTS_URL}?filter=tuition&establishment_id=${establishment}`
|
||||||
)
|
);
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchRegistrationFees = (establishment) => {
|
export const fetchRegistrationFees = (establishment) => {
|
||||||
return fetch(
|
return fetchWithAuth(
|
||||||
`${BE_SCHOOL_FEES_URL}?filter=registration&establishment_id=${establishment}`
|
`${BE_SCHOOL_FEES_URL}?filter=registration&establishment_id=${establishment}`
|
||||||
)
|
);
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchTuitionFees = (establishment) => {
|
export const fetchTuitionFees = (establishment) => {
|
||||||
return fetch(
|
return fetchWithAuth(
|
||||||
`${BE_SCHOOL_FEES_URL}?filter=tuition&establishment_id=${establishment}`
|
`${BE_SCHOOL_FEES_URL}?filter=tuition&establishment_id=${establishment}`
|
||||||
)
|
);
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchRegistrationPaymentPlans = (establishment) => {
|
export const fetchRegistrationPaymentPlans = (establishment) => {
|
||||||
return fetch(
|
return fetchWithAuth(
|
||||||
`${BE_SCHOOL_PAYMENT_PLANS_URL}?filter=registration&establishment_id=${establishment}`
|
`${BE_SCHOOL_PAYMENT_PLANS_URL}?filter=registration&establishment_id=${establishment}`
|
||||||
)
|
);
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchTuitionPaymentPlans = (establishment) => {
|
export const fetchTuitionPaymentPlans = (establishment) => {
|
||||||
return fetch(
|
return fetchWithAuth(
|
||||||
`${BE_SCHOOL_PAYMENT_PLANS_URL}?filter=tuition&establishment_id=${establishment}`
|
`${BE_SCHOOL_PAYMENT_PLANS_URL}?filter=tuition&establishment_id=${establishment}`
|
||||||
)
|
);
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchRegistrationPaymentModes = (establishment) => {
|
export const fetchRegistrationPaymentModes = (establishment) => {
|
||||||
return fetch(
|
return fetchWithAuth(
|
||||||
`${BE_SCHOOL_PAYMENT_MODES_URL}?filter=registration&establishment_id=${establishment}`
|
`${BE_SCHOOL_PAYMENT_MODES_URL}?filter=registration&establishment_id=${establishment}`
|
||||||
)
|
);
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchTuitionPaymentModes = (establishment) => {
|
export const fetchTuitionPaymentModes = (establishment) => {
|
||||||
return fetch(
|
return fetchWithAuth(
|
||||||
`${BE_SCHOOL_PAYMENT_MODES_URL}?filter=tuition&establishment_id=${establishment}`
|
`${BE_SCHOOL_PAYMENT_MODES_URL}?filter=tuition&establishment_id=${establishment}`
|
||||||
)
|
);
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchEstablishment = (establishment) => {
|
export const fetchEstablishment = (establishment) => {
|
||||||
return fetch(`${BE_SCHOOL_ESTABLISHMENT_URL}/${establishment}`)
|
return fetchWithAuth(`${BE_SCHOOL_ESTABLISHMENT_URL}/${establishment}`);
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createDatas = (url, newData, csrfToken) => {
|
export const createDatas = (url, newData, csrfToken) => {
|
||||||
return fetch(url, {
|
return fetchWithAuth(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(newData),
|
body: JSON.stringify(newData),
|
||||||
credentials: 'include',
|
});
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateDatas = (url, id, updatedData, csrfToken) => {
|
export const updateDatas = (url, id, updatedData, csrfToken) => {
|
||||||
return fetch(`${url}/${id}`, {
|
return fetchWithAuth(`${url}/${id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(updatedData),
|
body: JSON.stringify(updatedData),
|
||||||
credentials: 'include',
|
});
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeDatas = (url, id, csrfToken) => {
|
export const removeDatas = (url, id, csrfToken) => {
|
||||||
return fetch(`${url}/${id}`, {
|
return fetchWithAuth(`${url}/${id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'Content-Type': 'application/json',
|
});
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { BE_SETTINGS_SMTP_URL } from '@/utils/Url';
|
import { BE_SETTINGS_SMTP_URL } from '@/utils/Url';
|
||||||
import { errorHandler, requestResponseHandler } from './actionsHandlers';
|
import { fetchWithAuth } from '@/utils/fetchWithAuth';
|
||||||
|
|
||||||
export const PENDING = 'pending';
|
export const PENDING = 'pending';
|
||||||
export const SUBSCRIBED = 'subscribed';
|
export const SUBSCRIBED = 'subscribed';
|
||||||
@ -10,26 +10,15 @@ export const fetchSmtpSettings = (csrfToken, establishment_id = null) => {
|
|||||||
if (establishment_id) {
|
if (establishment_id) {
|
||||||
url += `?establishment_id=${establishment_id}`;
|
url += `?establishment_id=${establishment_id}`;
|
||||||
}
|
}
|
||||||
return fetch(`${url}`, {
|
return fetchWithAuth(url, {
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'Content-Type': 'application/json',
|
});
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const editSmtpSettings = (data, csrfToken) => {
|
export const editSmtpSettings = (data, csrfToken) => {
|
||||||
return fetch(`${BE_SETTINGS_SMTP_URL}/`, {
|
return fetchWithAuth(`${BE_SETTINGS_SMTP_URL}/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
credentials: 'include',
|
});
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,20 +11,15 @@ import {
|
|||||||
} from '@/utils/Url';
|
} from '@/utils/Url';
|
||||||
|
|
||||||
import { CURRENT_YEAR_FILTER } from '@/utils/constants';
|
import { CURRENT_YEAR_FILTER } from '@/utils/constants';
|
||||||
import { errorHandler, requestResponseHandler } from './actionsHandlers';
|
import { fetchWithAuth, fetchWithAuthRaw } from '@/utils/fetchWithAuth';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
|
|
||||||
export const editStudentCompetencies = (data, csrfToken) => {
|
export const editStudentCompetencies = (data, csrfToken) => {
|
||||||
const request = new Request(`${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}`, {
|
return fetchWithAuth(`${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
headers: {
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
});
|
||||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchStudentCompetencies = (id, period) => {
|
export const fetchStudentCompetencies = (id, period) => {
|
||||||
@ -33,13 +28,7 @@ export const fetchStudentCompetencies = (id, period) => {
|
|||||||
? `${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}?student_id=${id}&period=${period}`
|
? `${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}?student_id=${id}&period=${period}`
|
||||||
: `${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}?student_id=${id}`;
|
: `${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}?student_id=${id}`;
|
||||||
|
|
||||||
const request = new Request(url, {
|
return fetchWithAuth(url);
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchRegisterForms = (
|
export const fetchRegisterForms = (
|
||||||
@ -53,37 +42,22 @@ export const fetchRegisterForms = (
|
|||||||
if (page !== '' && pageSize !== '') {
|
if (page !== '' && pageSize !== '') {
|
||||||
url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}?filter=${filter}&establishment_id=${establishment}&page=${page}&search=${search}`;
|
url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}?filter=${filter}&establishment_id=${establishment}&page=${page}&search=${search}`;
|
||||||
}
|
}
|
||||||
return fetch(url, {
|
return fetchWithAuth(url);
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchRegisterForm = (id) => {
|
export const fetchRegisterForm = (id) => {
|
||||||
return fetch(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`) // Utilisation de studentId au lieu de codeDI
|
return fetchWithAuth(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`); // Utilisation de studentId au lieu de codeDI
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
export const fetchLastGuardian = () => {
|
export const fetchLastGuardian = () => {
|
||||||
return fetch(`${BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL}`)
|
return fetchWithAuth(`${BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL}`);
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const editRegisterForm = (id, data, csrfToken) => {
|
export const editRegisterForm = (id, data, csrfToken) => {
|
||||||
return fetch(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`, {
|
return fetchWithAuth(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
body: data,
|
body: data,
|
||||||
credentials: 'include',
|
});
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const autoSaveRegisterForm = async (id, data, csrfToken) => {
|
export const autoSaveRegisterForm = async (id, data, csrfToken) => {
|
||||||
@ -106,15 +80,12 @@ export const autoSaveRegisterForm = async (id, data, csrfToken) => {
|
|||||||
}
|
}
|
||||||
autoSaveData.append('auto_save', 'true');
|
autoSaveData.append('auto_save', 'true');
|
||||||
|
|
||||||
return fetch(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`, {
|
return fetchWithAuth(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`, {
|
||||||
method: 'PATCH', // Utiliser PATCH pour les mises à jour partielles
|
method: 'PATCH', // Utiliser PATCH pour les mises à jour partielles
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
body: autoSaveData,
|
body: autoSaveData,
|
||||||
credentials: 'include',
|
|
||||||
})
|
})
|
||||||
.then(requestResponseHandler)
|
.then(() => {})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// Silent fail pour l'auto-save
|
// Silent fail pour l'auto-save
|
||||||
logger.debug('Auto-save failed silently');
|
logger.debug('Auto-save failed silently');
|
||||||
@ -127,62 +98,30 @@ export const autoSaveRegisterForm = async (id, data, csrfToken) => {
|
|||||||
|
|
||||||
export const createRegisterForm = (data, csrfToken) => {
|
export const createRegisterForm = (data, csrfToken) => {
|
||||||
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}`;
|
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}`;
|
||||||
return fetch(url, {
|
return fetchWithAuth(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
credentials: 'include',
|
});
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sendRegisterForm = (id) => {
|
export const sendRegisterForm = (id) => {
|
||||||
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/send`;
|
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/send`;
|
||||||
return fetch(url, {
|
return fetchWithAuth(url);
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resendRegisterForm = (id) => {
|
export const resendRegisterForm = (id) => {
|
||||||
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/resend`;
|
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/resend`;
|
||||||
return fetch(url, {
|
return fetchWithAuth(url);
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
export const archiveRegisterForm = (id) => {
|
export const archiveRegisterForm = (id) => {
|
||||||
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/archive`;
|
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/archive`;
|
||||||
return fetch(url, {
|
return fetchWithAuth(url);
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const searchStudents = (establishmentId, query) => {
|
export const searchStudents = (establishmentId, query) => {
|
||||||
const url = `${BE_SUBSCRIPTION_SEARCH_STUDENTS_URL}/?establishment_id=${establishmentId}&q=${encodeURIComponent(query)}`;
|
const url = `${BE_SUBSCRIPTION_SEARCH_STUDENTS_URL}/?establishment_id=${establishmentId}&q=${encodeURIComponent(query)}`;
|
||||||
return fetch(url, {
|
return fetchWithAuth(url);
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchStudents = (establishment, id = null, status = null) => {
|
export const fetchStudents = (establishment, id = null, status = null) => {
|
||||||
@ -195,153 +134,68 @@ export const fetchStudents = (establishment, id = null, status = null) => {
|
|||||||
url += `&status=${status}`;
|
url += `&status=${status}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const request = new Request(url, {
|
return fetchWithAuth(url);
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchChildren = (id, establishment) => {
|
export const fetchChildren = (id, establishment) => {
|
||||||
const request = new Request(
|
return fetchWithAuth(
|
||||||
`${BE_SUBSCRIPTION_CHILDRENS_URL}/${id}?establishment_id=${establishment}`,
|
`${BE_SUBSCRIPTION_CHILDRENS_URL}/${id}?establishment_id=${establishment}`
|
||||||
{
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getRegisterFormFileTemplate(fileId) {
|
export async function getRegisterFormFileTemplate(fileId) {
|
||||||
const response = await fetch(
|
return fetchWithAuth(
|
||||||
`${BE_SUBSCRIPTION_REGISTERFORM_FILE_TEMPLATE_URL}/${fileId}`,
|
`${BE_SUBSCRIPTION_REGISTERFORM_FILE_TEMPLATE_URL}/${fileId}`
|
||||||
{
|
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to fetch file template');
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchSchoolFileTemplatesFromRegistrationFiles = async (id) => {
|
export const fetchSchoolFileTemplatesFromRegistrationFiles = (id) => {
|
||||||
const response = await fetch(
|
return fetchWithAuth(
|
||||||
`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/school_file_templates`,
|
`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/school_file_templates`
|
||||||
{
|
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(
|
|
||||||
'Erreur lors de la récupération des fichiers associés au groupe'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchParentFileTemplatesFromRegistrationFiles = async (id) => {
|
export const fetchParentFileTemplatesFromRegistrationFiles = (id) => {
|
||||||
const response = await fetch(
|
return fetchWithAuth(
|
||||||
`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/parent_file_templates`,
|
`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/parent_file_templates`
|
||||||
{
|
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(
|
|
||||||
'Erreur lors de la récupération des fichiers associés au groupe'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dissociateGuardian = async (studentId, guardianId) => {
|
export const dissociateGuardian = (studentId, guardianId) => {
|
||||||
const response = await fetch(
|
return fetchWithAuth(
|
||||||
`${BE_SUBSCRIPTION_STUDENTS_URL}/${studentId}/guardians/${guardianId}/dissociate`,
|
`${BE_SUBSCRIPTION_STUDENTS_URL}/${studentId}/guardians/${guardianId}/dissociate`,
|
||||||
{
|
{
|
||||||
credentials: 'include',
|
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
// Extraire le message d'erreur du backend
|
|
||||||
const errorData = await response.json();
|
|
||||||
const errorMessage =
|
|
||||||
errorData?.error || 'Une erreur est survenue lors de la dissociation.';
|
|
||||||
|
|
||||||
// Jeter une erreur avec le message spécifique
|
|
||||||
throw new Error(errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchAbsences = (establishment) => {
|
export const fetchAbsences = (establishment) => {
|
||||||
return fetch(
|
return fetchWithAuth(
|
||||||
`${BE_SUBSCRIPTION_ABSENCES_URL}?establishment_id=${establishment}`
|
`${BE_SUBSCRIPTION_ABSENCES_URL}?establishment_id=${establishment}`
|
||||||
)
|
);
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createAbsences = (data, csrfToken) => {
|
export const createAbsences = (data, csrfToken) => {
|
||||||
return fetch(`${BE_SUBSCRIPTION_ABSENCES_URL}`, {
|
return fetchWithAuth(`${BE_SUBSCRIPTION_ABSENCES_URL}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
headers: {
|
});
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const editAbsences = (absenceId, payload, csrfToken) => {
|
export const editAbsences = (absenceId, payload, csrfToken) => {
|
||||||
return fetch(`${BE_SUBSCRIPTION_ABSENCES_URL}/${absenceId}`, {
|
return fetchWithAuth(`${BE_SUBSCRIPTION_ABSENCES_URL}/${absenceId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'Content-Type': 'application/json',
|
body: JSON.stringify(payload),
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload), // Sérialisez les données en JSON
|
|
||||||
credentials: 'include',
|
|
||||||
}).then((response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
return response.json().then((error) => {
|
|
||||||
throw new Error(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteAbsences = (id, csrfToken) => {
|
export const deleteAbsences = (id, csrfToken) => {
|
||||||
return fetch(`${BE_SUBSCRIPTION_ABSENCES_URL}/${id}`, {
|
return fetchWithAuthRaw(`${BE_SUBSCRIPTION_ABSENCES_URL}/${id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -352,16 +206,7 @@ export const deleteAbsences = (id, csrfToken) => {
|
|||||||
*/
|
*/
|
||||||
export const fetchRegistrationSchoolFileMasters = (establishmentId) => {
|
export const fetchRegistrationSchoolFileMasters = (establishmentId) => {
|
||||||
const url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}?establishment_id=${establishmentId}`;
|
const url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}?establishment_id=${establishmentId}`;
|
||||||
|
return fetchWithAuth(url);
|
||||||
return fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -373,22 +218,14 @@ export const fetchRegistrationSchoolFileMasters = (establishmentId) => {
|
|||||||
*/
|
*/
|
||||||
export const saveFormResponses = (templateId, formTemplateData, csrfToken) => {
|
export const saveFormResponses = (templateId, formTemplateData, csrfToken) => {
|
||||||
const url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}/${templateId}`;
|
const url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}/${templateId}`;
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
formTemplateData: formTemplateData,
|
formTemplateData: formTemplateData,
|
||||||
};
|
};
|
||||||
|
return fetchWithAuth(url, {
|
||||||
return fetch(url, {
|
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: { 'X-CSRFToken': csrfToken },
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
credentials: 'include',
|
});
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -398,14 +235,5 @@ export const saveFormResponses = (templateId, formTemplateData, csrfToken) => {
|
|||||||
*/
|
*/
|
||||||
export const fetchFormResponses = (templateId) => {
|
export const fetchFormResponses = (templateId) => {
|
||||||
const url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}/${templateId}`;
|
const url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}/${templateId}`;
|
||||||
|
return fetchWithAuth(url);
|
||||||
return fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -48,30 +48,6 @@ export default function FormRenderer({
|
|||||||
}
|
}
|
||||||
}, [initialValues, reset]);
|
}, [initialValues, reset]);
|
||||||
|
|
||||||
// Fonction utilitaire pour envoyer les données au backend
|
|
||||||
const sendFormDataToBackend = async (formData) => {
|
|
||||||
try {
|
|
||||||
// Cette fonction peut être remplacée par votre propre implémentation
|
|
||||||
// Exemple avec fetch:
|
|
||||||
const response = await fetch('/api/submit-form', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData,
|
|
||||||
// Les en-têtes sont automatiquement définis pour FormData
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Erreur HTTP ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
logger.debug('Envoi réussi:', result);
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Erreur lors de l'envoi:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = async (data) => {
|
const onSubmit = async (data) => {
|
||||||
logger.debug('=== DÉBUT onSubmit ===');
|
logger.debug('=== DÉBUT onSubmit ===');
|
||||||
logger.debug('Réponses :', data);
|
logger.debug('Réponses :', data);
|
||||||
|
|||||||
@ -1,27 +1,32 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
import { FE_USERS_LOGIN_URL, getRedirectUrlFromRole } from '@/utils/Url';
|
import { FE_USERS_LOGIN_URL, getRedirectUrlFromRole } from '@/utils/Url';
|
||||||
|
import Loader from '@/components/Loader';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
|
|
||||||
const ProtectedRoute = ({ children, requiredRight }) => {
|
const ProtectedRoute = ({ children, requiredRight }) => {
|
||||||
const { user, profileRole } = useEstablishment();
|
const { data: session, status } = useSession();
|
||||||
|
const { profileRole } = useEstablishment();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [hasRequiredRight, setHasRequiredRight] = useState(false);
|
const [hasRequiredRight, setHasRequiredRight] = useState(false);
|
||||||
|
|
||||||
// Vérifier si l'utilisateur a au moins un rôle correspondant au requiredRight
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
logger.debug({
|
// Ne pas agir tant que NextAuth charge la session
|
||||||
user,
|
if (status === 'loading') return;
|
||||||
profileRole,
|
|
||||||
requiredRight,
|
|
||||||
hasRequiredRight,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (user && profileRole !== null) {
|
logger.debug({ status, profileRole, requiredRight });
|
||||||
|
|
||||||
|
if (status === 'unauthenticated') {
|
||||||
|
router.push(FE_USERS_LOGIN_URL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// status === 'authenticated' — vérifier les droits
|
||||||
|
if (profileRole !== null && profileRole !== undefined) {
|
||||||
let requiredRightChecked = false;
|
let requiredRightChecked = false;
|
||||||
if (requiredRight && Array.isArray(requiredRight)) {
|
if (requiredRight && Array.isArray(requiredRight)) {
|
||||||
// Vérifier si l'utilisateur a le droit requis
|
|
||||||
requiredRightChecked = requiredRight.some(
|
requiredRightChecked = requiredRight.some(
|
||||||
(right) => profileRole === right
|
(right) => profileRole === right
|
||||||
);
|
);
|
||||||
@ -30,21 +35,18 @@ const ProtectedRoute = ({ children, requiredRight }) => {
|
|||||||
}
|
}
|
||||||
setHasRequiredRight(requiredRightChecked);
|
setHasRequiredRight(requiredRightChecked);
|
||||||
|
|
||||||
// Vérifier si l'utilisateur a le droit requis mais pas le bon role on le redirige la page d'accueil associé au role
|
|
||||||
if (!requiredRightChecked) {
|
if (!requiredRightChecked) {
|
||||||
const redirectUrl = getRedirectUrlFromRole(profileRole);
|
const redirectUrl = getRedirectUrlFromRole(profileRole);
|
||||||
if (redirectUrl !== null) {
|
if (redirectUrl) {
|
||||||
router.push(`${redirectUrl}`);
|
router.push(redirectUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// User non authentifié
|
|
||||||
router.push(`${FE_USERS_LOGIN_URL}`);
|
|
||||||
}
|
}
|
||||||
}, [user, profileRole]);
|
}, [status, profileRole, requiredRight]);
|
||||||
|
|
||||||
// Autoriser l'affichage si authentifié et rôle correct
|
if (status === 'loading' || !hasRequiredRight) return <Loader />;
|
||||||
return hasRequiredRight ? children : null;
|
|
||||||
|
return children;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProtectedRoute;
|
export default ProtectedRoute;
|
||||||
|
|||||||
@ -145,18 +145,19 @@ const TeachersSection = ({
|
|||||||
// Retourne le profil existant pour un email
|
// Retourne le profil existant pour un email
|
||||||
const getUsedProfileForEmail = (email) => {
|
const getUsedProfileForEmail = (email) => {
|
||||||
// On cherche tous les profils dont l'email correspond
|
// On cherche tous les profils dont l'email correspond
|
||||||
const matchingProfiles = profiles.filter(p => p.email === email);
|
const matchingProfiles = profiles.filter((p) => p.email === email);
|
||||||
|
|
||||||
// On retourne le premier profil correspondant (ou undefined)
|
// On retourne le premier profil correspondant (ou undefined)
|
||||||
const result = matchingProfiles.length > 0 ? matchingProfiles[0] : undefined;
|
const result =
|
||||||
|
matchingProfiles.length > 0 ? matchingProfiles[0] : undefined;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Met à jour le formData et newTeacher si besoin
|
// Met à jour le formData et newTeacher si besoin
|
||||||
const updateFormData = (data) => {
|
const updateFormData = (data) => {
|
||||||
setFormData(prev => ({ ...prev, ...data }));
|
setFormData((prev) => ({ ...prev, ...data }));
|
||||||
if (newTeacher) setNewTeacher(prev => ({ ...prev, ...data }));
|
if (newTeacher) setNewTeacher((prev) => ({ ...prev, ...data }));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Récupération des messages d'erreur pour un champ donné
|
// Récupération des messages d'erreur pour un champ donné
|
||||||
@ -171,7 +172,9 @@ const TeachersSection = ({
|
|||||||
const existingProfile = getUsedProfileForEmail(email);
|
const existingProfile = getUsedProfileForEmail(email);
|
||||||
|
|
||||||
if (existingProfile) {
|
if (existingProfile) {
|
||||||
logger.info(`Adresse email déjà utilisée pour le profil ${existingProfile.id}`);
|
logger.info(
|
||||||
|
`Adresse email déjà utilisée pour le profil ${existingProfile.id}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFormData({
|
updateFormData({
|
||||||
@ -202,8 +205,8 @@ const TeachersSection = ({
|
|||||||
logger.debug('[DELETE] Suppression teacher id:', id);
|
logger.debug('[DELETE] Suppression teacher id:', id);
|
||||||
return handleDelete(id)
|
return handleDelete(id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setTeachers(prevTeachers =>
|
setTeachers((prevTeachers) =>
|
||||||
prevTeachers.filter(teacher => teacher.id !== id)
|
prevTeachers.filter((teacher) => teacher.id !== id)
|
||||||
);
|
);
|
||||||
logger.debug('[DELETE] Teacher supprimé:', id);
|
logger.debug('[DELETE] Teacher supprimé:', id);
|
||||||
})
|
})
|
||||||
@ -247,13 +250,13 @@ const TeachersSection = ({
|
|||||||
createdTeacher.profile
|
createdTeacher.profile
|
||||||
) {
|
) {
|
||||||
newProfileId = createdTeacher.profile;
|
newProfileId = createdTeacher.profile;
|
||||||
foundProfile = profiles.find(p => p.id === newProfileId);
|
foundProfile = profiles.find((p) => p.id === newProfileId);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTeachers([createdTeacher, ...teachers]);
|
setTeachers([createdTeacher, ...teachers]);
|
||||||
setNewTeacher(null);
|
setNewTeacher(null);
|
||||||
setLocalErrors({});
|
setLocalErrors({});
|
||||||
setFormData(prev => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
existingProfileId: newProfileId,
|
existingProfileId: newProfileId,
|
||||||
}));
|
}));
|
||||||
@ -419,7 +422,7 @@ const TeachersSection = ({
|
|||||||
case 'SPECIALITES':
|
case 'SPECIALITES':
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center space-x-2 flex-wrap">
|
<div className="flex justify-center space-x-2 flex-wrap">
|
||||||
{teacher.specialities_details.map((speciality) => (
|
{(teacher.specialities_details ?? []).map((speciality) => (
|
||||||
<SpecialityItem
|
<SpecialityItem
|
||||||
key={speciality.id}
|
key={speciality.id}
|
||||||
speciality={speciality}
|
speciality={speciality}
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
|
'use client';
|
||||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||||
|
import { flushSync } from 'react-dom';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
|
|
||||||
const EstablishmentContext = createContext();
|
const EstablishmentContext = createContext();
|
||||||
@ -46,7 +48,8 @@ export const EstablishmentProvider = ({ children }) => {
|
|||||||
const storedUser = sessionStorage.getItem('user');
|
const storedUser = sessionStorage.getItem('user');
|
||||||
return storedUser ? JSON.parse(storedUser) : null;
|
return storedUser ? JSON.parse(storedUser) : null;
|
||||||
});
|
});
|
||||||
const [selectedEstablishmentLogo, setSelectedEstablishmentLogoState] = useState(() => {
|
const [selectedEstablishmentLogo, setSelectedEstablishmentLogoState] =
|
||||||
|
useState(() => {
|
||||||
const storedLogo = sessionStorage.getItem('selectedEstablishmentLogo');
|
const storedLogo = sessionStorage.getItem('selectedEstablishmentLogo');
|
||||||
return storedLogo ? JSON.parse(storedLogo) : null;
|
return storedLogo ? JSON.parse(storedLogo) : null;
|
||||||
});
|
});
|
||||||
@ -106,8 +109,6 @@ export const EstablishmentProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
const user = session.user;
|
const user = session.user;
|
||||||
logger.debug('User Session:', user);
|
logger.debug('User Session:', user);
|
||||||
setUser(user);
|
|
||||||
logger.debug('Establishments User= ', user);
|
|
||||||
const userEstablishments = user.roles.map((role, i) => ({
|
const userEstablishments = user.roles.map((role, i) => ({
|
||||||
id: role.establishment__id,
|
id: role.establishment__id,
|
||||||
name: role.establishment__name,
|
name: role.establishment__name,
|
||||||
@ -117,13 +118,19 @@ export const EstablishmentProvider = ({ children }) => {
|
|||||||
role_id: i,
|
role_id: i,
|
||||||
role_type: role.role_type,
|
role_type: role.role_type,
|
||||||
}));
|
}));
|
||||||
setEstablishments(userEstablishments);
|
|
||||||
logger.debug('Establishments', user.roleIndexLoginDefault);
|
|
||||||
if (user.roles && user.roles.length > 0) {
|
|
||||||
let roleIndexDefault = 0;
|
let roleIndexDefault = 0;
|
||||||
|
if (user.roles && user.roles.length > 0) {
|
||||||
if (userEstablishments.length > user.roleIndexLoginDefault) {
|
if (userEstablishments.length > user.roleIndexLoginDefault) {
|
||||||
roleIndexDefault = user.roleIndexLoginDefault;
|
roleIndexDefault = user.roleIndexLoginDefault;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// flushSync force React à commiter tous les setState de manière synchrone
|
||||||
|
// avant que endInitFunctionHandler (router.push) soit appelé.
|
||||||
|
// Sans ça, ProtectedRoute verrait user=null au premier rendu post-navigation.
|
||||||
|
flushSync(() => {
|
||||||
|
setUser(user);
|
||||||
|
setEstablishments(userEstablishments);
|
||||||
|
if (user.roles && user.roles.length > 0) {
|
||||||
setSelectedRoleId(roleIndexDefault);
|
setSelectedRoleId(roleIndexDefault);
|
||||||
if (userEstablishments.length > 0) {
|
if (userEstablishments.length > 0) {
|
||||||
setSelectedEstablishmentId(userEstablishments[roleIndexDefault].id);
|
setSelectedEstablishmentId(userEstablishments[roleIndexDefault].id);
|
||||||
@ -138,6 +145,10 @@ export const EstablishmentProvider = ({ children }) => {
|
|||||||
);
|
);
|
||||||
setProfileRole(userEstablishments[roleIndexDefault].role_type);
|
setProfileRole(userEstablishments[roleIndexDefault].role_type);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
logger.debug('Establishments', user.roleIndexLoginDefault);
|
||||||
|
if (user.roles && user.roles.length > 0) {
|
||||||
if (endInitFunctionHandler) {
|
if (endInitFunctionHandler) {
|
||||||
const role = session.user.roles[roleIndexDefault].role_type;
|
const role = session.user.roles[roleIndexDefault].role_type;
|
||||||
endInitFunctionHandler(role);
|
endInitFunctionHandler(role);
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
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 { getJWT, refreshJWT } from '@/app/actions/authAction';
|
|
||||||
import jwt_decode from 'jsonwebtoken';
|
import jwt_decode from 'jsonwebtoken';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
|
|
||||||
@ -13,19 +12,32 @@ const options = {
|
|||||||
email: { label: 'Email', type: 'email' },
|
email: { label: 'Email', type: 'email' },
|
||||||
password: { label: 'Password', type: 'password' },
|
password: { label: 'Password', type: 'password' },
|
||||||
},
|
},
|
||||||
authorize: async (credentials, req) => {
|
authorize: async (credentials) => {
|
||||||
|
// URL calculée ici (pas au niveau module) pour garantir que NEXT_PUBLIC_API_URL est chargé
|
||||||
|
const loginUrl = `${process.env.NEXT_PUBLIC_API_URL}/Auth/login`;
|
||||||
try {
|
try {
|
||||||
const data = {
|
const res = await fetch(loginUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
// Connection: close évite le SocketError undici lié au keep-alive vers Daphne
|
||||||
|
Connection: 'close',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
email: credentials.email,
|
email: credentials.email,
|
||||||
password: credentials.password,
|
password: credentials.password,
|
||||||
};
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
const user = await getJWT(data);
|
if (!res.ok) {
|
||||||
|
const body = await res.json().catch(() => ({}));
|
||||||
if (user) {
|
throw new Error(body?.errorMessage || 'Identifiants invalides');
|
||||||
return user;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const user = await res.json();
|
||||||
|
return user || null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
logger.error('Authorize error:', error.message);
|
||||||
throw new Error(error.message || 'Invalid credentials');
|
throw new Error(error.message || 'Invalid credentials');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -33,8 +45,10 @@ const options = {
|
|||||||
],
|
],
|
||||||
session: {
|
session: {
|
||||||
strategy: 'jwt',
|
strategy: 'jwt',
|
||||||
maxAge: 30 * 24 * 60 * 60, // 30 jours
|
maxAge: 60 * 60, // 1 Hour
|
||||||
updateAge: 24 * 60 * 60, // 24 heures
|
// 0 = réécrire le cookie à chaque fois que le token change (indispensable avec
|
||||||
|
// un access token Django de 15 min, sinon le cookie expiré reste en place)
|
||||||
|
updateAge: 0,
|
||||||
},
|
},
|
||||||
cookies: {
|
cookies: {
|
||||||
sessionToken: {
|
sessionToken: {
|
||||||
@ -64,25 +78,61 @@ const options = {
|
|||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token expiré, essayer de le rafraîchir
|
// Token Django expiré (lifetime = 15 min), essayer de le rafraîchir
|
||||||
|
logger.info('JWT: access token expiré, tentative de refresh');
|
||||||
|
|
||||||
|
if (!token.refresh) {
|
||||||
|
logger.error('JWT: refresh token absent dans la session');
|
||||||
|
return { ...token, error: 'RefreshTokenError' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshUrl = `${process.env.NEXT_PUBLIC_API_URL}/Auth/refreshJWT`;
|
||||||
|
if (!process.env.NEXT_PUBLIC_API_URL) {
|
||||||
|
logger.error('JWT: NEXT_PUBLIC_API_URL non défini, refresh impossible');
|
||||||
|
return { ...token, error: 'RefreshTokenError' };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await refreshJWT({ refresh: token.refresh });
|
const res = await fetch(refreshUrl, {
|
||||||
if (response && response?.token) {
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
// Connection: close évite le SocketError undici lié au keep-alive vers Daphne
|
||||||
|
Connection: 'close',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ refresh: token.refresh }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.json().catch(() => ({}));
|
||||||
|
logger.error('JWT: refresh échoué', { status: res.status, body });
|
||||||
|
throw new Error(`Refresh HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await res.json();
|
||||||
|
if (!response?.token) {
|
||||||
|
logger.error('JWT: réponse refresh sans token', { response });
|
||||||
|
throw new Error('Réponse refresh invalide');
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('JWT: refresh réussi');
|
||||||
return {
|
return {
|
||||||
...token,
|
...token,
|
||||||
token: response.token,
|
token: response.token,
|
||||||
refresh: response.refresh,
|
refresh: response.refresh,
|
||||||
tokenExpires: jwt_decode.decode(response.token).exp * 1000,
|
tokenExpires: jwt_decode.decode(response.token).exp * 1000,
|
||||||
|
error: undefined,
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
throw new Error('Failed to refresh token');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Refresh token failed:', error);
|
logger.error('JWT: refresh token failed', { message: error.message });
|
||||||
return token;
|
return { ...token, error: 'RefreshTokenError' };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async session({ session, token }) {
|
async session({ session, token }) {
|
||||||
|
if (token?.error === 'RefreshTokenError') {
|
||||||
|
session.error = 'RefreshTokenError';
|
||||||
|
return session;
|
||||||
|
}
|
||||||
if (token && token?.token) {
|
if (token && token?.token) {
|
||||||
const { user_id, email, roles, roleIndexLoginDefault } =
|
const { user_id, email, roles, roleIndexLoginDefault } =
|
||||||
jwt_decode.decode(token.token);
|
jwt_decode.decode(token.token);
|
||||||
|
|||||||
101
Front-End/src/utils/fetchWithAuth.js
Normal file
101
Front-End/src/utils/fetchWithAuth.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { getSession } from 'next-auth/react';
|
||||||
|
import {
|
||||||
|
requestResponseHandler,
|
||||||
|
errorHandler,
|
||||||
|
triggerSignOut,
|
||||||
|
} from '@/app/actions/actionsHandlers';
|
||||||
|
import logger from '@/utils/logger';
|
||||||
|
|
||||||
|
// Déduplique les appels concurrents à getSession() :
|
||||||
|
// si plusieurs fetchWithAuth() partent en même temps (chargement de page),
|
||||||
|
// ils partagent la même promesse au lieu de déclencher N refreshs JWT en parallèle.
|
||||||
|
let _pendingSessionPromise = null;
|
||||||
|
|
||||||
|
const getSessionOnce = () => {
|
||||||
|
if (!_pendingSessionPromise) {
|
||||||
|
_pendingSessionPromise = getSession().finally(() => {
|
||||||
|
_pendingSessionPromise = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return _pendingSessionPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le token JWT Bearer depuis la session NextAuth.
|
||||||
|
* @returns {Promise<string|null>}
|
||||||
|
*/
|
||||||
|
export const getAuthToken = async () => {
|
||||||
|
const session = await getSessionOnce();
|
||||||
|
if (!session) {
|
||||||
|
logger.warn('getAuthToken: session nulle, aucun token envoyé');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (session?.error === 'RefreshTokenError') {
|
||||||
|
logger.warn(
|
||||||
|
'getAuthToken: RefreshTokenError détecté, déconnexion en cours'
|
||||||
|
);
|
||||||
|
await triggerSignOut();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!session?.user?.token) {
|
||||||
|
logger.warn('getAuthToken: session présente mais token absent', {
|
||||||
|
session,
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return session.user.token;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper de fetch qui injecte automatiquement le header Authorization Bearer
|
||||||
|
* depuis la session NextAuth, puis passe la réponse dans requestResponseHandler.
|
||||||
|
*
|
||||||
|
* - Ajoute Content-Type: application/json par défaut (sauf si le body est FormData)
|
||||||
|
* - Ajoute credentials: 'include' par défaut
|
||||||
|
* - Les options.headers passées en paramètre surchargent les défauts (ex: X-CSRFToken)
|
||||||
|
*
|
||||||
|
* @param {string} url
|
||||||
|
* @param {RequestInit} options
|
||||||
|
* @returns {Promise<any>} Corps de la réponse désérialisé
|
||||||
|
*/
|
||||||
|
export const fetchWithAuth = async (url, options = {}) => {
|
||||||
|
const token = await getAuthToken();
|
||||||
|
const isFormData = options.body instanceof FormData;
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
...(isFormData ? {} : { 'Content-Type': 'application/json' }),
|
||||||
|
...options.headers,
|
||||||
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return fetch(url, {
|
||||||
|
credentials: 'include',
|
||||||
|
...options,
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
.then(requestResponseHandler)
|
||||||
|
.catch(errorHandler);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variante de fetchWithAuth qui retourne la Response brute sans passer
|
||||||
|
* par requestResponseHandler. Utile quand l'appelant gère lui-même response.ok.
|
||||||
|
*
|
||||||
|
* @param {string} url
|
||||||
|
* @param {RequestInit} options
|
||||||
|
* @returns {Promise<Response>}
|
||||||
|
*/
|
||||||
|
export const fetchWithAuthRaw = async (url, options = {}) => {
|
||||||
|
const token = await getAuthToken();
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
...options.headers,
|
||||||
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return fetch(url, {
|
||||||
|
credentials: 'include',
|
||||||
|
...options,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -21,4 +21,5 @@ DB_PASSWORD="postgres"
|
|||||||
DB_HOST="database"
|
DB_HOST="database"
|
||||||
DB_PORT="5432"
|
DB_PORT="5432"
|
||||||
URL_DJANGO="http://localhost:8080"
|
URL_DJANGO="http://localhost:8080"
|
||||||
SECRET_KEY="<SIGNINGKEY>"
|
SECRET_KEY="<SECRET_KEY>"
|
||||||
|
WEBHOOK_API_KEY="<WEBHOOK_API_KEY>"
|
||||||
Reference in New Issue
Block a user