feat: add JWT auth foundation - /api/v1/ with admin login, refresh, me, health endpoints
- Add djangorestframework-simplejwt==5.3.1 to requirements-docker.txt - Configure REST_FRAMEWORK with JWTAuthentication and SIMPLE_JWT settings - Create admin_api Django app with AdminLoginView, MeView, HealthView - Wire /api/v1/ routes without touching existing /api/ mobile endpoints - Resolve pre-existing events migration conflict (0010_merge) - Superuser admin created for initial authentication
This commit is contained in:
0
admin_api/__init__.py
Normal file
0
admin_api/__init__.py
Normal file
4
admin_api/apps.py
Normal file
4
admin_api/apps.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
class AdminApiConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'admin_api'
|
||||||
18
admin_api/serializers.py
Normal file
18
admin_api/serializers.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
|
name = serializers.SerializerMethodField()
|
||||||
|
role = serializers.SerializerMethodField()
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ['id', 'email', 'username', 'name', 'role']
|
||||||
|
def get_name(self, obj):
|
||||||
|
return f"{obj.first_name} {obj.last_name}".strip() or obj.username
|
||||||
|
def get_role(self, obj):
|
||||||
|
if obj.is_superuser:
|
||||||
|
return 'superadmin'
|
||||||
|
if obj.is_staff:
|
||||||
|
return 'admin'
|
||||||
|
return getattr(obj, 'role', 'user')
|
||||||
10
admin_api/urls.py
Normal file
10
admin_api/urls.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from rest_framework_simplejwt.views import TokenRefreshView
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('admin/auth/login/', views.AdminLoginView.as_view(), name='admin_login'),
|
||||||
|
path('auth/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
||||||
|
path('auth/me/', views.MeView.as_view(), name='auth_me'),
|
||||||
|
path('health/', views.HealthView.as_view(), name='health'),
|
||||||
|
]
|
||||||
52
admin_api/views.py
Normal file
52
admin_api/views.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
from django.contrib.auth import authenticate, get_user_model
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
from rest_framework_simplejwt.views import TokenRefreshView
|
||||||
|
from django.db import connection
|
||||||
|
from .serializers import UserSerializer
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
class AdminLoginView(APIView):
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
def post(self, request):
|
||||||
|
identifier = request.data.get('username') or request.data.get('email')
|
||||||
|
password = request.data.get('password')
|
||||||
|
if not identifier or not password:
|
||||||
|
return Response({'error': 'username/email and password required'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
# Try username first, then email
|
||||||
|
user = authenticate(request, username=identifier, password=password)
|
||||||
|
if not user:
|
||||||
|
try:
|
||||||
|
u = User.objects.get(email=identifier)
|
||||||
|
user = authenticate(request, username=u.username, password=password)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
pass
|
||||||
|
if not user:
|
||||||
|
return Response({'error': 'Invalid credentials'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
if not user.is_active:
|
||||||
|
return Response({'error': 'Account is disabled'}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
refresh = RefreshToken.for_user(user)
|
||||||
|
return Response({
|
||||||
|
'access': str(refresh.access_token),
|
||||||
|
'refresh': str(refresh),
|
||||||
|
'user': UserSerializer(user).data,
|
||||||
|
})
|
||||||
|
|
||||||
|
class MeView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
def get(self, request):
|
||||||
|
return Response({'user': UserSerializer(request.user).data})
|
||||||
|
|
||||||
|
class HealthView(APIView):
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
def get(self, request):
|
||||||
|
try:
|
||||||
|
connection.ensure_connection()
|
||||||
|
db_status = 'ok'
|
||||||
|
except Exception:
|
||||||
|
db_status = 'error'
|
||||||
|
return Response({'status': 'ok', 'db': db_status})
|
||||||
@@ -9,7 +9,7 @@ SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'change-me-in-production')
|
|||||||
#
|
#
|
||||||
# ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '*').split(',')
|
# ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '*').split(',')
|
||||||
|
|
||||||
DEBUG = True
|
DEBUG = False
|
||||||
|
|
||||||
ALLOWED_HOSTS = [
|
ALLOWED_HOSTS = [
|
||||||
'*'
|
'*'
|
||||||
@@ -33,7 +33,10 @@ INSTALLED_APPS = [
|
|||||||
'bookings',
|
'bookings',
|
||||||
'banking_operations',
|
'banking_operations',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
'rest_framework.authtoken'
|
'rest_framework.authtoken',
|
||||||
|
'rest_framework_simplejwt',
|
||||||
|
'admin_api',
|
||||||
|
'django_summernote'
|
||||||
]
|
]
|
||||||
|
|
||||||
INSTALLED_APPS += [
|
INSTALLED_APPS += [
|
||||||
@@ -54,10 +57,17 @@ MIDDLEWARE = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
CORS_ALLOWED_ORIGINS = [
|
CORS_ALLOWED_ORIGINS = [
|
||||||
|
"http://localhost:5178",
|
||||||
|
"http://localhost:5179",
|
||||||
"http://localhost:5173",
|
"http://localhost:5173",
|
||||||
|
"http://localhost:3001",
|
||||||
|
"http://localhost:3000",
|
||||||
"https://prototype.eventifyplus.com",
|
"https://prototype.eventifyplus.com",
|
||||||
"https://eventifyplus.com",
|
"https://eventifyplus.com",
|
||||||
"https://mv.eventifyplus.com"
|
"https://mv.eventifyplus.com",
|
||||||
|
"https://db.eventifyplus.com",
|
||||||
|
"https://test.eventifyplus.com",
|
||||||
|
"https://em.eventifyplus.com"
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = 'eventify.urls'
|
ROOT_URLCONF = 'eventify.urls'
|
||||||
@@ -82,8 +92,12 @@ WSGI_APPLICATION = 'eventify.wsgi.application'
|
|||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': os.environ.get('DB_ENGINE', 'django.db.backends.sqlite3'),
|
||||||
'NAME': BASE_DIR / 'db.sqlite3',
|
'NAME': os.environ.get('DB_NAME', str(BASE_DIR / 'db.sqlite3')),
|
||||||
|
'USER': os.environ.get('DB_USER', ''),
|
||||||
|
'PASSWORD': os.environ.get('DB_PASS', ''),
|
||||||
|
'HOST': os.environ.get('DB_HOST', ''),
|
||||||
|
'PORT': os.environ.get('DB_PORT', '5432'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +132,8 @@ STATICFILES_DIRS = [BASE_DIR / 'static']
|
|||||||
MEDIA_URL = '/media/'
|
MEDIA_URL = '/media/'
|
||||||
MEDIA_ROOT = BASE_DIR / 'media'
|
MEDIA_ROOT = BASE_DIR / 'media'
|
||||||
|
|
||||||
|
X_FRAME_OPTIONS = 'SAMEORIGIN'
|
||||||
|
|
||||||
AUTH_USER_MODEL = 'accounts.User'
|
AUTH_USER_MODEL = 'accounts.User'
|
||||||
|
|
||||||
LOGIN_URL = 'login'
|
LOGIN_URL = 'login'
|
||||||
@@ -125,4 +141,37 @@ LOGIN_REDIRECT_URL = 'dashboard'
|
|||||||
LOGOUT_REDIRECT_URL = 'login'
|
LOGOUT_REDIRECT_URL = 'login'
|
||||||
|
|
||||||
# EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
# EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||||
# DEFAULT_FROM_EMAIL = 'no-reply@example.com'
|
# DEFAULT_FROM_EMAIL = 'no-reply@example.com'
|
||||||
|
|
||||||
|
SUMMERNOTE_THEME = 'bs5'
|
||||||
|
|
||||||
|
# Reverse proxy / CSRF fix
|
||||||
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
|
'https://db.eventifyplus.com',
|
||||||
|
'https://uat.eventifyplus.com',
|
||||||
|
'https://test.eventifyplus.com',
|
||||||
|
'https://eventifyplus.com',
|
||||||
|
]
|
||||||
|
|
||||||
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
|
USE_X_FORWARDED_HOST = True
|
||||||
|
|
||||||
|
# --- JWT Auth (Phase 1) ---
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
|
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||||
|
),
|
||||||
|
'DEFAULT_PERMISSION_CLASSES': (
|
||||||
|
'rest_framework.permissions.IsAuthenticated',
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
SIMPLE_JWT = {
|
||||||
|
'ACCESS_TOKEN_LIFETIME': timedelta(days=1),
|
||||||
|
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
|
||||||
|
'AUTH_HEADER_TYPES': ('Bearer',),
|
||||||
|
'USER_ID_FIELD': 'id',
|
||||||
|
'USER_ID_CLAIM': 'user_id',
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,7 +35,10 @@ urlpatterns = [
|
|||||||
path('partner/', include('partner.urls')),
|
path('partner/', include('partner.urls')),
|
||||||
path('banking/', include('banking_operations.urls')),
|
path('banking/', include('banking_operations.urls')),
|
||||||
path('api/', include('mobile_api.urls')),
|
path('api/', include('mobile_api.urls')),
|
||||||
|
path('api/v1/', include('admin_api.urls')),
|
||||||
# path('web-api/', include('web_api.urls')),
|
# path('web-api/', include('web_api.urls')),
|
||||||
|
|
||||||
|
path('summernote/', include('django_summernote.urls')),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|||||||
14
events/migrations/0010_merge_20260324_1443.py
Normal file
14
events/migrations/0010_merge_20260324_1443.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Generated by Django 4.2.21 on 2026-03-24 14:43
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('events', '0007_add_is_featured_is_top_event'),
|
||||||
|
('events', '0009_alter_event_id_alter_eventimages_id'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
]
|
||||||
9
requirements-docker.txt
Normal file
9
requirements-docker.txt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Django==4.2.21
|
||||||
|
Pillow==10.1.0
|
||||||
|
django-summernote
|
||||||
|
djangorestframework==3.14.0
|
||||||
|
django-cors-headers==4.3.0
|
||||||
|
gunicorn==21.2.0
|
||||||
|
django-extensions==3.2.3
|
||||||
|
psycopg2-binary==2.9.9
|
||||||
|
djangorestframework-simplejwt==5.3.1
|
||||||
Reference in New Issue
Block a user