diff --git a/apps/api/decorators.py b/apps/api/decorators.py new file mode 100644 index 0000000..917d5f4 --- /dev/null +++ b/apps/api/decorators.py @@ -0,0 +1,42 @@ +from functools import wraps +from django.http import HttpResponseForbidden +from django.contrib.auth.models import AnonymousUser +from django.views.decorators.csrf import csrf_exempt +from rest_framework.authtoken.models import Token + + +def swagger_auth_required(view_func): + """ + Decorator that requires either admin authentication or valid swagger token + """ + @csrf_exempt + @wraps(view_func) + def _wrapped_view(request, *args, **kwargs): + # Check if user is admin + if request.user and request.user.is_authenticated and request.user.is_staff: + return view_func(request, *args, **kwargs) + + # Check swagger token in session + swagger_token = request.session.get('swagger_token') + if swagger_token: + try: + token_obj = Token.objects.get(key=swagger_token) + if token_obj.user.is_active: + return view_func(request, *args, **kwargs) + except Token.DoesNotExist: + pass + + # Check Authorization header + auth_header = request.META.get('HTTP_AUTHORIZATION', '') + if auth_header.startswith('Token '): + token = auth_header.split(' ')[1] + try: + token_obj = Token.objects.get(key=token) + if token_obj.user.is_active: + return view_func(request, *args, **kwargs) + except Token.DoesNotExist: + pass + + return HttpResponseForbidden("Access denied. Admin authentication or valid token required.") + + return _wrapped_view \ No newline at end of file diff --git a/apps/api/permissions.py b/apps/api/permissions.py new file mode 100644 index 0000000..3540a1e --- /dev/null +++ b/apps/api/permissions.py @@ -0,0 +1,60 @@ +from rest_framework import permissions +from rest_framework.authtoken.models import Token +from django.contrib.auth.models import AnonymousUser + + +class SwaggerTokenPermission(permissions.BasePermission): + """ + Custom permission for Swagger that allows access to authenticated users via token + or admin users via session authentication + """ + + def has_permission(self, request, view): + # Check if user is admin (for session-based access) + if request.user and request.user.is_authenticated and request.user.is_staff: + return True + + # Check for token in session (from our custom auth system) + swagger_token = request.session.get('swagger_token') + if swagger_token: + try: + token_obj = Token.objects.get(key=swagger_token) + if token_obj.user.is_active: + return True + except Token.DoesNotExist: + pass + + # Check for Authorization header + auth_header = request.META.get('HTTP_AUTHORIZATION', '') + if auth_header.startswith('Token '): + token = auth_header.split(' ')[1] + try: + token_obj = Token.objects.get(key=token) + if token_obj.user.is_active: + return True + except Token.DoesNotExist: + pass + + return False + + +class IsAdminOrSwaggerToken(permissions.BasePermission): + """ + Permission that allows access to admin users or users with valid swagger token + """ + + def has_permission(self, request, view): + # Allow admin users + if request.user and request.user.is_authenticated and request.user.is_staff: + return True + + # Check swagger token in session + swagger_token = request.session.get('swagger_token') + if swagger_token: + try: + token_obj = Token.objects.get(key=swagger_token) + return token_obj.user.is_active + except Token.DoesNotExist: + pass + + return False \ No newline at end of file diff --git a/apps/api/views/swagger_views.py b/apps/api/views/swagger_views.py index 1f2dc15..d76db8c 100644 --- a/apps/api/views/swagger_views.py +++ b/apps/api/views/swagger_views.py @@ -3,17 +3,26 @@ from django.views import View from django.contrib import messages from django.contrib.admin.views.decorators import staff_member_required from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt +from django.urls import reverse from rest_framework.authtoken.models import Token -@method_decorator(staff_member_required, name='dispatch') +@method_decorator([staff_member_required, csrf_exempt], name='dispatch') class CustomSwaggerView(View): """ Custom Swagger UI view with authentication banner Requires admin login to access """ def get(self, request): + # Generate dynamic swagger spec URL based on current language + try: + swagger_spec_url = reverse('schema-json', kwargs={'format': '.json'}) + except: + # Fallback to hardcoded URL if reverse fails + swagger_spec_url = '/en/swagger.json' + context = { - 'swagger_spec_url': '/en/swagger.json', # Adjust based on your URL structure + 'swagger_spec_url': swagger_spec_url, 'request': request, } return render(request, 'swagger/ui.html', context) diff --git a/config/urls.py b/config/urls.py index 7959ba0..e452dfe 100644 --- a/config/urls.py +++ b/config/urls.py @@ -39,8 +39,10 @@ from filer import views # Import custom API views from apps.api.views import CustomAPIDocumentationView, CustomSwaggerView, SwaggerTokenAuthView, clear_swagger_auth +from apps.api.permissions import IsAdminOrSwaggerToken +from apps.api.decorators import swagger_auth_required -# Restricted schema view for admin users only +# Restricted schema view for admin users and authenticated swagger users schema_view = get_schema_view( openapi.Info( title="Imam Javad API", @@ -50,7 +52,7 @@ schema_view = get_schema_view( license=openapi.License(name="MIT License"), ), public=False, - permission_classes=(permissions.IsAdminUser,), + permission_classes=(IsAdminOrSwaggerToken,), ) @@ -105,11 +107,11 @@ swagger_urlpatterns = [ path('swagger-auth/', SwaggerTokenAuthView.as_view(), name='swagger-token-auth'), path('swagger-auth/clear/', clear_swagger_auth, name='clear-swagger-auth'), re_path(r'^swagger(?P\.json|\.yaml)$', - staff_member_required(schema_view.without_ui(cache_timeout=0)), + swagger_auth_required(schema_view.without_ui(cache_timeout=0)), name='schema-json'), path('swagger/', CustomSwaggerView.as_view(), name='schema-swagger-ui'), re_path(r'^redoc/$', - staff_member_required(schema_view.with_ui('redoc', cache_timeout=0)), + swagger_auth_required(schema_view.with_ui('redoc', cache_timeout=0)), name='schema-redoc'), ] diff --git a/templates/swagger/ui.html b/templates/swagger/ui.html index afb9535..f14d931 100644 --- a/templates/swagger/ui.html +++ b/templates/swagger/ui.html @@ -1,9 +1,11 @@ +{% load static %} Imam Javad API - Swagger UI + {% csrf_token %} @@ -310,6 +312,13 @@ {% if request.session.swagger_token %} request.headers['Authorization'] = 'Token {{ request.session.swagger_token }}'; {% endif %} + + // Add CSRF token for Django + const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]'); + if (csrfToken) { + request.headers['X-CSRFToken'] = csrfToken.value; + } + return request; }, responseInterceptor: function(response) {