mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-04 01:51:28 +00:00
feat: Securisation du Backend
This commit is contained in:
@ -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.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 import status
|
||||
from rest_framework.parsers import MultiPartParser, FormParser
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from django.db import models
|
||||
from .models import Conversation, ConversationParticipant, Message, UserPresence
|
||||
from Auth.models import Profile, ProfileRole
|
||||
@ -25,6 +26,8 @@ logger = logging.getLogger(__name__)
|
||||
# ====================== MESSAGERIE INSTANTANÉE ======================
|
||||
|
||||
class InstantConversationListView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
"""
|
||||
API pour lister les conversations instantanées d'un utilisateur
|
||||
"""
|
||||
@ -34,7 +37,8 @@ class InstantConversationListView(APIView):
|
||||
)
|
||||
def get(self, request, user_id=None):
|
||||
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(
|
||||
participants__participant=user,
|
||||
@ -50,6 +54,8 @@ class InstantConversationListView(APIView):
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
class InstantConversationCreateView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
"""
|
||||
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)
|
||||
|
||||
class InstantMessageListView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
"""
|
||||
API pour lister les messages d'une conversation
|
||||
"""
|
||||
@ -79,23 +87,19 @@ class InstantMessageListView(APIView):
|
||||
conversation = Conversation.objects.get(id=conversation_id)
|
||||
messages = conversation.messages.filter(is_deleted=False).order_by('created_at')
|
||||
|
||||
# Récupérer l'utilisateur actuel depuis les paramètres de requête
|
||||
user_id = request.GET.get('user_id')
|
||||
user = None
|
||||
if user_id:
|
||||
try:
|
||||
user = Profile.objects.get(id=user_id)
|
||||
except Profile.DoesNotExist:
|
||||
pass
|
||||
# Utiliser l'utilisateur authentifié — ignorer user_id du paramètre (protection IDOR)
|
||||
user = request.user
|
||||
|
||||
serializer = MessageSerializer(messages, many=True, context={'user': user})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except Conversation.DoesNotExist:
|
||||
return Response({'error': 'Conversation not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
except Exception as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
except Exception:
|
||||
return Response({'error': 'Erreur interne du serveur'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
class InstantMessageCreateView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
"""
|
||||
API pour envoyer un nouveau message instantané
|
||||
"""
|
||||
@ -116,21 +120,20 @@ class InstantMessageCreateView(APIView):
|
||||
def post(self, request):
|
||||
try:
|
||||
conversation_id = request.data.get('conversation_id')
|
||||
sender_id = request.data.get('sender_id')
|
||||
content = request.data.get('content', '').strip()
|
||||
message_type = request.data.get('message_type', 'text')
|
||||
|
||||
if not all([conversation_id, sender_id, content]):
|
||||
if not all([conversation_id, content]):
|
||||
return Response(
|
||||
{'error': 'conversation_id, sender_id, and content are required'},
|
||||
{'error': 'conversation_id and content are required'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Vérifier que la conversation existe
|
||||
conversation = Conversation.objects.get(id=conversation_id)
|
||||
|
||||
# Vérifier que l'expéditeur existe et peut envoyer dans cette conversation
|
||||
sender = Profile.objects.get(id=sender_id)
|
||||
# L'expéditeur est toujours l'utilisateur authentifié (protection IDOR)
|
||||
sender = request.user
|
||||
participant = ConversationParticipant.objects.filter(
|
||||
conversation=conversation,
|
||||
participant=sender,
|
||||
@ -172,10 +175,12 @@ class InstantMessageCreateView(APIView):
|
||||
return Response({'error': 'Conversation not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
except Profile.DoesNotExist:
|
||||
return Response({'error': 'Sender not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
except Exception as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
except Exception:
|
||||
return Response({'error': 'Erreur interne du serveur'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
class InstantMarkAsReadView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
"""
|
||||
API pour marquer une conversation comme lue
|
||||
"""
|
||||
@ -190,15 +195,16 @@ class InstantMarkAsReadView(APIView):
|
||||
),
|
||||
responses={200: openapi.Response('Success')}
|
||||
)
|
||||
def post(self, request, conversation_id):
|
||||
def post(self, request):
|
||||
try:
|
||||
user_id = request.data.get('user_id')
|
||||
if not user_id:
|
||||
return Response({'error': 'user_id is required'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Utiliser l'utilisateur authentifié — ignorer user_id du body (protection IDOR)
|
||||
# conversation_id est lu depuis le body (pas depuis l'URL)
|
||||
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(
|
||||
conversation_id=conversation_id,
|
||||
participant_id=user_id,
|
||||
participant=request.user,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
@ -209,10 +215,12 @@ class InstantMarkAsReadView(APIView):
|
||||
|
||||
except ConversationParticipant.DoesNotExist:
|
||||
return Response({'error': 'Participant not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
except Exception as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
except Exception:
|
||||
return Response({'error': 'Erreur interne du serveur'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
class UserPresenceView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
"""
|
||||
API pour gérer la présence des utilisateurs
|
||||
"""
|
||||
@ -245,8 +253,8 @@ class UserPresenceView(APIView):
|
||||
|
||||
except Profile.DoesNotExist:
|
||||
return Response({'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
except Exception as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
except Exception:
|
||||
return Response({'error': 'Erreur interne du serveur'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Récupère le statut de présence d'un utilisateur",
|
||||
@ -266,10 +274,12 @@ class UserPresenceView(APIView):
|
||||
|
||||
except Profile.DoesNotExist:
|
||||
return Response({'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
except Exception as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
except Exception:
|
||||
return Response({'error': 'Erreur interne du serveur'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
class FileUploadView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
"""
|
||||
API pour l'upload de fichiers dans la messagerie instantanée
|
||||
"""
|
||||
@ -301,18 +311,17 @@ class FileUploadView(APIView):
|
||||
try:
|
||||
file = request.FILES.get('file')
|
||||
conversation_id = request.data.get('conversation_id')
|
||||
sender_id = request.data.get('sender_id')
|
||||
|
||||
if not file:
|
||||
return Response({'error': 'Aucun fichier fourni'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if not conversation_id or not sender_id:
|
||||
return Response({'error': 'conversation_id et sender_id requis'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if not conversation_id:
|
||||
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:
|
||||
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
|
||||
if not ConversationParticipant.objects.filter(
|
||||
@ -368,10 +377,12 @@ class FileUploadView(APIView):
|
||||
'filePath': file_path
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
except Exception as e:
|
||||
return Response({'error': f'Erreur lors de l\'upload: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
except Exception:
|
||||
return Response({'error': "Erreur lors de l'upload"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
class InstantRecipientSearchView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
"""
|
||||
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)
|
||||
|
||||
class InstantConversationDeleteView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
"""
|
||||
API pour supprimer (désactiver) une conversation instantanée
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user