Fix admin login 500 when clients omit the trailing slash.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Shayan Azadi
2026-08-17 18:49:15 +03:30
parent 26b83dabac
commit e8423facaa
5 changed files with 66 additions and 9 deletions

View File

@@ -25,7 +25,10 @@ DJANGO_PORT=8000
# CORS Settings (comma-separated origins)
# 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)
# Leave empty locally if you create the user manually

View File

@@ -3,6 +3,7 @@ from datetime import timedelta
from django.contrib.auth import get_user_model
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import override_settings
from django.utils import timezone
from PIL import Image
from rest_framework import status
@@ -343,6 +344,26 @@ class AdminLoginAPITests(APITestCase):
self.assertIn('token', response.data)
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):
response = self.client.post(
'/api/admin/login/',

View File

@@ -1,5 +1,6 @@
from django.urls import path, include
from django.urls import include, re_path
from rest_framework.routers import DefaultRouter
from .views import (
ContactUsViewSet,
CompositionViewSet,
@@ -9,14 +10,20 @@ from .views import (
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'compositions', CompositionViewSet, basename='composition')
router.register(r'campaigns', CampaignViewSet, basename='campaign')
urlpatterns = [
path('admin/login/', admin_login, name='admin-login'),
path('admin/logout/', admin_logout, name='admin-logout'),
path('admin/me/', admin_me, name='admin-me'),
path('', include(router.urls)),
re_path(r'^admin/login/?$', admin_login, name='admin-login'),
re_path(r'^admin/logout/?$', admin_logout, name='admin-logout'),
re_path(r'^admin/me/?$', admin_me, name='admin-me'),
re_path(r'^', include(router.urls)),
]

View File

@@ -1,5 +1,10 @@
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.permissions import AllowAny, IsAuthenticated, IsAdminUser
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
@@ -17,6 +22,7 @@ from .serializers import (
@api_view(['POST'])
@authentication_classes([])
@permission_classes([AllowAny])
def admin_login(request):
"""

View File

@@ -149,12 +149,32 @@ MEDIA_ROOT = BASE_DIR / 'media'
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 = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny',