Fix admin login 500 when clients omit the trailing slash.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -25,7 +25,10 @@ DJANGO_PORT=8000
|
|||||||
|
|
||||||
# CORS Settings (comma-separated origins)
|
# CORS Settings (comma-separated origins)
|
||||||
# Example: CORS_ALLOWED_ORIGINS=http://YOUR_SERVER_IP:9123,https://your-frontend.com
|
# Example: CORS_ALLOWED_ORIGINS=http://YOUR_SERVER_IP:9123,https://your-frontend.com
|
||||||
CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000
|
CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://panel.zoneco.org,https://zoneco.org,https://www.zoneco.org
|
||||||
|
|
||||||
|
# HTTPS origins trusted for CSRF (needed behind nginx / Arvan)
|
||||||
|
CSRF_TRUSTED_ORIGINS=https://zoneco.org,https://www.zoneco.org,https://panel.zoneco.org
|
||||||
|
|
||||||
# Admin bootstrap (used by Docker entrypoint on server)
|
# Admin bootstrap (used by Docker entrypoint on server)
|
||||||
# Leave empty locally if you create the user manually
|
# Leave empty locally if you create the user manually
|
||||||
|
|||||||
21
api/tests.py
21
api/tests.py
@@ -3,6 +3,7 @@ from datetime import timedelta
|
|||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
from django.test import override_settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@@ -343,6 +344,26 @@ class AdminLoginAPITests(APITestCase):
|
|||||||
self.assertIn('token', response.data)
|
self.assertIn('token', response.data)
|
||||||
self.assertEqual(response.data['user']['username'], self.admin_username)
|
self.assertEqual(response.data['user']['username'], self.admin_username)
|
||||||
|
|
||||||
|
def test_admin_login_without_trailing_slash(self):
|
||||||
|
"""Axios/fetch omit the slash; this used to 500 with APPEND_SLASH + DEBUG."""
|
||||||
|
response = self.client.post(
|
||||||
|
'/api/admin/login',
|
||||||
|
{'username': self.admin_username, 'password': self.admin_password},
|
||||||
|
format='json',
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertIn('token', response.data)
|
||||||
|
|
||||||
|
def test_admin_login_without_trailing_slash_debug(self):
|
||||||
|
with override_settings(DEBUG=True):
|
||||||
|
response = self.client.post(
|
||||||
|
'/api/admin/login',
|
||||||
|
{'username': self.admin_username, 'password': self.admin_password},
|
||||||
|
format='json',
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertIn('token', response.data)
|
||||||
|
|
||||||
def test_admin_login_wrong_password(self):
|
def test_admin_login_wrong_password(self):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
'/api/admin/login/',
|
'/api/admin/login/',
|
||||||
|
|||||||
19
api/urls.py
19
api/urls.py
@@ -1,5 +1,6 @@
|
|||||||
from django.urls import path, include
|
from django.urls import include, re_path
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
from .views import (
|
from .views import (
|
||||||
ContactUsViewSet,
|
ContactUsViewSet,
|
||||||
CompositionViewSet,
|
CompositionViewSet,
|
||||||
@@ -9,14 +10,20 @@ from .views import (
|
|||||||
admin_me,
|
admin_me,
|
||||||
)
|
)
|
||||||
|
|
||||||
router = DefaultRouter()
|
|
||||||
|
class OptionalSlashRouter(DefaultRouter):
|
||||||
|
"""Match /api/resource and /api/resource/ so POST clients do not hit APPEND_SLASH."""
|
||||||
|
trailing_slash = '/?'
|
||||||
|
|
||||||
|
|
||||||
|
router = OptionalSlashRouter()
|
||||||
router.register(r'contact-us', ContactUsViewSet, basename='contact-us')
|
router.register(r'contact-us', ContactUsViewSet, basename='contact-us')
|
||||||
router.register(r'compositions', CompositionViewSet, basename='composition')
|
router.register(r'compositions', CompositionViewSet, basename='composition')
|
||||||
router.register(r'campaigns', CampaignViewSet, basename='campaign')
|
router.register(r'campaigns', CampaignViewSet, basename='campaign')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/login/', admin_login, name='admin-login'),
|
re_path(r'^admin/login/?$', admin_login, name='admin-login'),
|
||||||
path('admin/logout/', admin_logout, name='admin-logout'),
|
re_path(r'^admin/logout/?$', admin_logout, name='admin-logout'),
|
||||||
path('admin/me/', admin_me, name='admin-me'),
|
re_path(r'^admin/me/?$', admin_me, name='admin-me'),
|
||||||
path('', include(router.urls)),
|
re_path(r'^', include(router.urls)),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
from rest_framework import viewsets, status
|
from rest_framework import viewsets, status
|
||||||
from rest_framework.decorators import action, api_view, permission_classes
|
from rest_framework.decorators import (
|
||||||
|
action,
|
||||||
|
api_view,
|
||||||
|
authentication_classes,
|
||||||
|
permission_classes,
|
||||||
|
)
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.permissions import AllowAny, IsAuthenticated, IsAdminUser
|
from rest_framework.permissions import AllowAny, IsAuthenticated, IsAdminUser
|
||||||
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||||
@@ -17,6 +22,7 @@ from .serializers import (
|
|||||||
|
|
||||||
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
|
@authentication_classes([])
|
||||||
@permission_classes([AllowAny])
|
@permission_classes([AllowAny])
|
||||||
def admin_login(request):
|
def admin_login(request):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -149,12 +149,32 @@ MEDIA_ROOT = BASE_DIR / 'media'
|
|||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
# Nginx / Arvan terminate TLS. Without this, Django builds http:// URLs and
|
||||||
|
# APPEND_SLASH / CSRF see the request as HTTP.
|
||||||
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
|
USE_X_FORWARDED_HOST = True
|
||||||
|
|
||||||
|
csrf_origins_env = os.environ.get('CSRF_TRUSTED_ORIGINS', '')
|
||||||
|
CSRF_TRUSTED_ORIGINS = [o.strip() for o in csrf_origins_env.split(',') if o.strip()]
|
||||||
|
if not CSRF_TRUSTED_ORIGINS:
|
||||||
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
|
'https://zoneco.org',
|
||||||
|
'https://www.zoneco.org',
|
||||||
|
'https://panel.zoneco.org',
|
||||||
|
]
|
||||||
|
if DEBUG:
|
||||||
|
CSRF_TRUSTED_ORIGINS += [
|
||||||
|
'http://localhost:5173',
|
||||||
|
'http://localhost:3000',
|
||||||
|
'http://127.0.0.1:5173',
|
||||||
|
'http://127.0.0.1:3000',
|
||||||
|
]
|
||||||
|
|
||||||
# REST Framework configuration
|
# REST Framework configuration
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||||
'rest_framework.authentication.TokenAuthentication',
|
'rest_framework.authentication.TokenAuthentication',
|
||||||
'rest_framework.authentication.BasicAuthentication',
|
'rest_framework.authentication.BasicAuthentication',
|
||||||
'rest_framework.authentication.SessionAuthentication',
|
|
||||||
],
|
],
|
||||||
'DEFAULT_PERMISSION_CLASSES': [
|
'DEFAULT_PERMISSION_CLASSES': [
|
||||||
'rest_framework.permissions.AllowAny',
|
'rest_framework.permissions.AllowAny',
|
||||||
|
|||||||
Reference in New Issue
Block a user