From 37001f8e70c8a15d8f143d424dc0733c33e9ed0f Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 24 Mar 2026 14:46:03 +0000 Subject: [PATCH] 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 --- admin_api/__init__.py | 0 admin_api/apps.py | 4 ++ admin_api/serializers.py | 18 ++++++ admin_api/urls.py | 10 +++ admin_api/views.py | 52 ++++++++++++++++ eventify/settings.py | 61 +++++++++++++++++-- eventify/urls.py | 3 + events/migrations/0010_merge_20260324_1443.py | 14 +++++ requirements-docker.txt | 9 +++ 9 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 admin_api/__init__.py create mode 100644 admin_api/apps.py create mode 100644 admin_api/serializers.py create mode 100644 admin_api/urls.py create mode 100644 admin_api/views.py create mode 100644 events/migrations/0010_merge_20260324_1443.py create mode 100644 requirements-docker.txt diff --git a/admin_api/__init__.py b/admin_api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/admin_api/apps.py b/admin_api/apps.py new file mode 100644 index 0000000..a4a51bb --- /dev/null +++ b/admin_api/apps.py @@ -0,0 +1,4 @@ +from django.apps import AppConfig +class AdminApiConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'admin_api' diff --git a/admin_api/serializers.py b/admin_api/serializers.py new file mode 100644 index 0000000..f203062 --- /dev/null +++ b/admin_api/serializers.py @@ -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') diff --git a/admin_api/urls.py b/admin_api/urls.py new file mode 100644 index 0000000..df64669 --- /dev/null +++ b/admin_api/urls.py @@ -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'), +] diff --git a/admin_api/views.py b/admin_api/views.py new file mode 100644 index 0000000..f66fb68 --- /dev/null +++ b/admin_api/views.py @@ -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}) diff --git a/eventify/settings.py b/eventify/settings.py index 6f21b6a..6b9284b 100644 --- a/eventify/settings.py +++ b/eventify/settings.py @@ -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(',') -DEBUG = True +DEBUG = False ALLOWED_HOSTS = [ '*' @@ -33,7 +33,10 @@ INSTALLED_APPS = [ 'bookings', 'banking_operations', 'rest_framework', - 'rest_framework.authtoken' + 'rest_framework.authtoken', + 'rest_framework_simplejwt', + 'admin_api', + 'django_summernote' ] INSTALLED_APPS += [ @@ -54,10 +57,17 @@ MIDDLEWARE = [ ] CORS_ALLOWED_ORIGINS = [ + "http://localhost:5178", + "http://localhost:5179", "http://localhost:5173", + "http://localhost:3001", + "http://localhost:3000", "https://prototype.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' @@ -82,8 +92,12 @@ WSGI_APPLICATION = 'eventify.wsgi.application' DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + 'ENGINE': os.environ.get('DB_ENGINE', 'django.db.backends.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_ROOT = BASE_DIR / 'media' +X_FRAME_OPTIONS = 'SAMEORIGIN' + AUTH_USER_MODEL = 'accounts.User' LOGIN_URL = 'login' @@ -125,4 +141,37 @@ LOGIN_REDIRECT_URL = 'dashboard' LOGOUT_REDIRECT_URL = 'login' # EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' -# DEFAULT_FROM_EMAIL = 'no-reply@example.com' \ No newline at end of file +# 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', +} diff --git a/eventify/urls.py b/eventify/urls.py index b7f32e4..420cdcc 100644 --- a/eventify/urls.py +++ b/eventify/urls.py @@ -35,7 +35,10 @@ urlpatterns = [ path('partner/', include('partner.urls')), path('banking/', include('banking_operations.urls')), path('api/', include('mobile_api.urls')), + path('api/v1/', include('admin_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) diff --git a/events/migrations/0010_merge_20260324_1443.py b/events/migrations/0010_merge_20260324_1443.py new file mode 100644 index 0000000..1582e74 --- /dev/null +++ b/events/migrations/0010_merge_20260324_1443.py @@ -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 = [ + ] diff --git a/requirements-docker.txt b/requirements-docker.txt new file mode 100644 index 0000000..29174d3 --- /dev/null +++ b/requirements-docker.txt @@ -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