Merge remote-tracking branch 'origin/develop' into N3WTS-5-Historique-ACA

This commit is contained in:
N3WT DE COMPET
2026-04-04 13:54:37 +02:00
31 changed files with 2180 additions and 576 deletions

View File

@ -3,6 +3,7 @@ from django.urls import path, re_path
from .views import (
DomainListCreateView, DomainDetailView,
CategoryListCreateView, CategoryDetailView,
ServeFileView,
)
urlpatterns = [
@ -11,4 +12,6 @@ urlpatterns = [
re_path(r'^categories$', CategoryListCreateView.as_view(), name="category_list_create"),
re_path(r'^categories/(?P<id>[0-9]+)$', CategoryDetailView.as_view(), name="category_detail"),
path('serve-file/', ServeFileView.as_view(), name="serve_file"),
]

View File

@ -1,3 +1,8 @@
import os
import mimetypes
from django.conf import settings
from django.http import FileResponse
from django.http.response import JsonResponse
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect
from django.utils.decorators import method_decorator
@ -117,3 +122,55 @@ class CategoryDetailView(APIView):
return JsonResponse({'message': 'Deleted'}, safe=False)
except Category.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
class ServeFileView(APIView):
"""Sert les fichiers media de manière sécurisée avec authentification JWT."""
permission_classes = [IsAuthenticated]
def get(self, request):
file_path = request.query_params.get('path', '')
if not file_path:
return JsonResponse(
{'error': 'Le paramètre "path" est requis'},
status=status.HTTP_400_BAD_REQUEST,
)
# Nettoyer le préfixe /data/ si présent
if file_path.startswith('/data/'):
file_path = file_path[len('/data/'):]
elif file_path.startswith('data/'):
file_path = file_path[len('data/'):]
# Construire le chemin absolu et le résoudre pour éliminer les traversals
absolute_path = os.path.realpath(
os.path.join(settings.MEDIA_ROOT, file_path)
)
# Protection contre le path traversal
media_root = os.path.realpath(settings.MEDIA_ROOT)
if not absolute_path.startswith(media_root + os.sep) and absolute_path != media_root:
return JsonResponse(
{'error': 'Accès non autorisé'},
status=status.HTTP_403_FORBIDDEN,
)
if not os.path.isfile(absolute_path):
return JsonResponse(
{'error': 'Fichier introuvable'},
status=status.HTTP_404_NOT_FOUND,
)
content_type, _ = mimetypes.guess_type(absolute_path)
if content_type is None:
content_type = 'application/octet-stream'
response = FileResponse(
open(absolute_path, 'rb'),
content_type=content_type,
)
response['Content-Disposition'] = (
f'inline; filename="{os.path.basename(absolute_path)}"'
)
return response