Refactor: api file endpoint for auth
This commit is contained in:
parent
bd2791c155
commit
91e7ea2e81
5 changed files with 156 additions and 26 deletions
46
backend/api/authentication/middleware.py
Normal file
46
backend/api/authentication/middleware.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
# api/authentication/middleware.py
|
||||||
|
|
||||||
|
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||||
|
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
|
||||||
|
|
||||||
|
|
||||||
|
class JWTParamMiddleware:
|
||||||
|
"""
|
||||||
|
Middleware that allows JWT authentication via query parameters.
|
||||||
|
|
||||||
|
This middleware extracts a JWT token from a query parameter named 'token'
|
||||||
|
and authenticates the user if the token is valid.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
self.jwt_auth = JWTAuthentication()
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
self._authenticate_token_param(request)
|
||||||
|
response = self.get_response(request)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _authenticate_token_param(self, request):
|
||||||
|
# Don't authenticate if already authenticated via headers
|
||||||
|
if hasattr(request, "user") and request.user.is_authenticated:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get token from the query parameter
|
||||||
|
token = request.GET.get("token")
|
||||||
|
if not token:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Validate the token
|
||||||
|
try:
|
||||||
|
validated_token = self.jwt_auth.get_validated_token(token)
|
||||||
|
user = self.jwt_auth.get_user(validated_token)
|
||||||
|
|
||||||
|
# Set the authenticated user on the request
|
||||||
|
request.user = user
|
||||||
|
|
||||||
|
# Also set auth in DRF format for API views
|
||||||
|
request._auth = validated_token
|
||||||
|
except (InvalidToken, TokenError):
|
||||||
|
# Don't raise exceptions, just leave as anonymous
|
||||||
|
pass
|
|
@ -5,28 +5,51 @@ from apps.files.models import PostFileModel
|
||||||
class PostFileSerializer(serializers.ModelSerializer):
|
class PostFileSerializer(serializers.ModelSerializer):
|
||||||
"""Serializer for PostFileModel."""
|
"""Serializer for PostFileModel."""
|
||||||
|
|
||||||
|
filename = serializers.SerializerMethodField()
|
||||||
|
thumbnails = serializers.SerializerMethodField()
|
||||||
|
download_url = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PostFileModel
|
model = PostFileModel
|
||||||
fields = ["hash_blake3", "file_type", "file", "thumbnail"]
|
fields = [
|
||||||
# Add any other fields you need
|
"hash_blake3",
|
||||||
|
"file_type",
|
||||||
|
"file",
|
||||||
|
"thumbnail",
|
||||||
|
"filename",
|
||||||
|
"thumbnails",
|
||||||
|
"download_url",
|
||||||
|
]
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def get_filename(self, obj):
|
||||||
"""Customize the representation of the model."""
|
|
||||||
representation = super().to_representation(instance)
|
|
||||||
|
|
||||||
# Add file name from related model
|
|
||||||
try:
|
try:
|
||||||
representation["filename"] = instance.name.first().filename
|
return obj.name.first().filename
|
||||||
except (AttributeError, IndexError):
|
except (AttributeError, IndexError):
|
||||||
representation["filename"] = "Unknown"
|
return "Unknown"
|
||||||
|
|
||||||
# Add URLs for different thumbnail sizes
|
def get_thumbnails(self, obj):
|
||||||
base_url = f"/api/files/{instance.hash_blake3}/"
|
base_url = f"/api/files/{obj.hash_blake3}/"
|
||||||
thumbnails = {}
|
thumbnails = {}
|
||||||
for size_key in THUMBNAIL_SIZES:
|
for size_key in ["sx", "sm", "md", "lg", "xl"]:
|
||||||
thumbnails[size_key] = f"{base_url}?t={size_key}"
|
thumbnails[size_key] = f"{base_url}?t={size_key}"
|
||||||
|
return thumbnails
|
||||||
|
|
||||||
representation["thumbnails"] = thumbnails
|
def get_download_url(self, obj):
|
||||||
representation["download_url"] = f"{base_url}?d=0"
|
return f"/api/files/{obj.hash_blake3}/?d=0"
|
||||||
|
|
||||||
return representation
|
|
||||||
|
class FileResponseSerializer(serializers.Serializer):
|
||||||
|
"""
|
||||||
|
Dummy serializer for file response schema documentation.
|
||||||
|
This is only used for OpenAPI schema generation and will never be used to serialize data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
file = serializers.FileField(help_text="The file content")
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorResponseSerializer(serializers.Serializer):
|
||||||
|
"""
|
||||||
|
Serializer for error responses.
|
||||||
|
"""
|
||||||
|
|
||||||
|
error = serializers.CharField(help_text="Error message")
|
||||||
|
|
|
@ -3,7 +3,7 @@ from .views import FileServeView, FileDetailView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Serve the actual file
|
# Serve the actual file
|
||||||
path("files/<str:file_hash>/", FileServeView.as_view(), name="serve_file"),
|
path("<str:file_hash>/", FileServeView.as_view(), name="serve_file"),
|
||||||
# Get file metadata
|
# Get file metadata
|
||||||
path("files/<str:file_hash>/info/", FileDetailView.as_view(), name="file_info"),
|
path("<str:file_hash>/info/", FileDetailView.as_view(), name="file_info"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -2,12 +2,17 @@ import os
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import FileResponse
|
from django.http import FileResponse
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.views import APIView
|
from rest_framework.generics import GenericAPIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiResponse
|
||||||
from sorl.thumbnail import get_thumbnail
|
from sorl.thumbnail import get_thumbnail
|
||||||
from apps.files.models import PostFileModel
|
from apps.files.models import PostFileModel
|
||||||
from .serializers import PostFileSerializer # You'll need to create this
|
from .serializers import (
|
||||||
|
PostFileSerializer,
|
||||||
|
FileResponseSerializer,
|
||||||
|
ErrorResponseSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
THUMBNAIL_SIZES = {
|
THUMBNAIL_SIZES = {
|
||||||
"sx": (64, ".thumb_64.jpg"),
|
"sx": (64, ".thumb_64.jpg"),
|
||||||
|
@ -18,13 +23,19 @@ THUMBNAIL_SIZES = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class FileServeView(APIView):
|
class FileServeView(GenericAPIView):
|
||||||
"""
|
"""
|
||||||
API view to serve content files for download or inline viewing.
|
API view to serve content files for download or inline viewing.
|
||||||
|
|
||||||
|
Authentication can be provided via:
|
||||||
|
1. Authorization header (JWT token)
|
||||||
|
2. 'token' query parameter (JWT token)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Uncomment the following line if authentication is required
|
# Set permissions as needed
|
||||||
# permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
serializer_class = FileResponseSerializer
|
||||||
|
queryset = PostFileModel.objects.all()
|
||||||
|
|
||||||
def get_thumbnail_file(self, source_path, size_key):
|
def get_thumbnail_file(self, source_path, size_key):
|
||||||
"""Generates and retrieves the thumbnail file."""
|
"""Generates and retrieves the thumbnail file."""
|
||||||
|
@ -36,6 +47,34 @@ class FileServeView(APIView):
|
||||||
), suffix
|
), suffix
|
||||||
return None, ""
|
return None, ""
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
parameters=[
|
||||||
|
OpenApiParameter(
|
||||||
|
name="d",
|
||||||
|
description="Download flag (0 = download, otherwise inline)",
|
||||||
|
required=False,
|
||||||
|
type=str,
|
||||||
|
),
|
||||||
|
OpenApiParameter(
|
||||||
|
name="t",
|
||||||
|
description="Thumbnail size (sx, sm, md, lg, xl)",
|
||||||
|
required=False,
|
||||||
|
type=str,
|
||||||
|
),
|
||||||
|
OpenApiParameter(
|
||||||
|
name="token",
|
||||||
|
description="JWT token for authentication (alternative to Authorization header)",
|
||||||
|
required=False,
|
||||||
|
type=str,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses={
|
||||||
|
200: OpenApiResponse(description="File returned successfully"),
|
||||||
|
401: ErrorResponseSerializer,
|
||||||
|
404: ErrorResponseSerializer,
|
||||||
|
500: ErrorResponseSerializer,
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self, request, file_hash):
|
def get(self, request, file_hash):
|
||||||
"""Handle GET requests for file serving."""
|
"""Handle GET requests for file serving."""
|
||||||
download = request.query_params.get("d") == "0"
|
download = request.query_params.get("d") == "0"
|
||||||
|
@ -82,14 +121,35 @@ class FileServeView(APIView):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class FileDetailView(APIView):
|
class FileDetailView(GenericAPIView):
|
||||||
"""
|
"""
|
||||||
API view to get file metadata without serving the actual file.
|
API view to get file metadata without serving the actual file.
|
||||||
|
|
||||||
|
Authentication can be provided via:
|
||||||
|
1. Authorization header (JWT token)
|
||||||
|
2. 'token' query parameter (JWT token)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Uncomment the following line if authentication is required
|
permission_classes = [IsAuthenticated]
|
||||||
# permission_classes = [IsAuthenticated]
|
serializer_class = PostFileSerializer
|
||||||
|
queryset = PostFileModel.objects.all()
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
parameters=[
|
||||||
|
OpenApiParameter(
|
||||||
|
name="token",
|
||||||
|
description="JWT token for authentication (alternative to Authorization header)",
|
||||||
|
required=False,
|
||||||
|
type=str,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
responses={
|
||||||
|
200: PostFileSerializer,
|
||||||
|
401: ErrorResponseSerializer,
|
||||||
|
404: ErrorResponseSerializer,
|
||||||
|
500: ErrorResponseSerializer,
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self, request, file_hash):
|
def get(self, request, file_hash):
|
||||||
"""Return file metadata."""
|
"""Return file metadata."""
|
||||||
try:
|
try:
|
||||||
|
@ -99,7 +159,7 @@ class FileDetailView(APIView):
|
||||||
{"error": "File not found"}, status=status.HTTP_404_NOT_FOUND
|
{"error": "File not found"}, status=status.HTTP_404_NOT_FOUND
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer = PostFileSerializer(obj_file)
|
serializer = self.get_serializer(obj_file)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
@ -69,6 +69,7 @@ MIDDLEWARE = [
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
"django.middleware.locale.LocaleMiddleware",
|
"django.middleware.locale.LocaleMiddleware",
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"api.authentication.middleware.JWTParamMiddleware",
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
]
|
]
|
||||||
|
|
Loading…
Add table
Reference in a new issue