diff --git a/admin_api/urls.py b/admin_api/urls.py index edfc3ab..e50b606 100644 --- a/admin_api/urls.py +++ b/admin_api/urls.py @@ -26,7 +26,11 @@ urlpatterns = [ path('events/stats/', views.EventStatsView.as_view(), name='event-stats'), path('events/', views.EventListView.as_view(), name='event-list'), path('events//', views.EventDetailView.as_view(), name='event-detail'), + path('events//update/', views.EventUpdateView.as_view(), name='event-update'), path('events//moderate/', views.EventModerationView.as_view(), name='event-moderate'), + path('events/create/', views.EventCreateView.as_view(), name='event-create'), + path('events/types/', views.EventTypesView.as_view(), name='event-types'), + path('events//primary-image/', views.EventPrimaryImageView.as_view(), name='event-primary-image'), path('financials/metrics/', views.FinancialMetricsView.as_view(), name='financial-metrics'), path('financials/transactions/', views.TransactionListView.as_view(), name='transaction-list'), path('financials/settlements/', views.SettlementListView.as_view(), name='settlement-list'), @@ -36,4 +40,9 @@ urlpatterns = [ path('reviews/', views.ReviewListView.as_view(), name='review-list'), path('reviews//moderate/', views.ReviewModerationView.as_view(), name='review-moderate'), path('reviews//', views.ReviewDeleteView.as_view(), name='review-delete'), + + # Payment gateway settings + path('settings/payment-gateway/active/', views.ActivePaymentGatewayView.as_view(), name='active-payment-gateway'), + path('settings/payment-gateways/', views.PaymentGatewaySettingsView.as_view(), name='payment-gateways'), + path('settings/payment-gateways//', views.PaymentGatewaySettingsView.as_view(), name='payment-gateway-detail'), ] \ No newline at end of file diff --git a/admin_api/views.py b/admin_api/views.py index 9ab4663..f45d500 100644 --- a/admin_api/views.py +++ b/admin_api/views.py @@ -545,6 +545,7 @@ class UserMetricsView(APIView): 'total': customer_qs.count(), 'active': customer_qs.filter(is_active=True).count(), 'suspended': customer_qs.filter(is_active=False).count(), + 'newThisWeek': customer_qs.filter(date_joined__date__gte=week_ago).count(), }) @@ -570,13 +571,26 @@ class UserListView(APIView): qs = qs.filter(role=backend_role) if q := request.GET.get('search'): qs = qs.filter( - Q(username__icontains=q) | Q(email__icontains=q) | - Q(first_name__icontains=q) | Q(last_name__icontains=q) | - Q(phone_number__icontains=q) + Q(first_name__icontains=search) | + Q(last_name__icontains=search) | + Q(email__icontains=search) | + Q(username__icontains=search) | + Q(phone_number__icontains=search) ) + + status_filter = request.query_params.get('status', '').strip().lower() + if status_filter == 'active': + qs = qs.filter(is_active=True) + elif status_filter == 'suspended': + qs = qs.filter(is_active=False) + + role_filter = request.query_params.get('role', '').strip() + if role_filter: + qs = qs.filter(role__iexact=role_filter) + try: - page = max(1, int(request.GET.get('page', 1))) - page_size = min(100, int(request.GET.get('page_size', 20))) + page = max(1, int(request.query_params.get('page', 1))) + page_size = min(100, int(request.query_params.get('page_size', 20))) except (ValueError, TypeError): page, page_size = 1, 20 total = qs.count() @@ -648,6 +662,41 @@ def _serialize_event(e): } + +def _serialize_event_detail(e): + """Full event serializer for detail view -- includes all fields + images.""" + base = _serialize_event(e) + base.update({ + 'name': e.name or '', + 'description': e.description or '', + 'endDate': e.end_date.isoformat() if e.end_date else '', + 'startTime': str(e.start_time or ''), + 'endTime': str(e.end_time or ''), + 'allYearEvent': bool(e.all_year_event), + 'latitude': str(e.latitude) if e.latitude else '', + 'longitude': str(e.longitude) if e.longitude else '', + 'pincode': e.pincode or '', + 'district': e.district or '', + 'state': e.state or '', + 'place': e.place or '', + 'isBookable': bool(e.is_bookable), + 'eventType': e.event_type_id, + 'importantInformation': e.important_information or '', + 'source': e.source or '', + 'cancelledReason': e.cancelled_reason or '', + 'outsideEventUrl': e.outside_event_url or '', + 'images': [ + { + 'id': img.id, + 'url': f'/media/{img.event_image}', + 'isPrimary': bool(img.is_primary), + } + for img in e.eventimages_set.all() + ], + }) + return base + + class EventStatsView(APIView): permission_classes = [IsAuthenticated] @@ -695,8 +744,64 @@ class EventDetailView(APIView): def get(self, request, pk): from events.models import Event from django.shortcuts import get_object_or_404 - e = get_object_or_404(Event.objects.select_related('partner'), pk=pk) - return Response(_serialize_event(e)) + e = get_object_or_404(Event.objects.select_related('partner').prefetch_related('eventimages_set'), pk=pk) + return Response(_serialize_event_detail(e)) + + +class EventUpdateView(APIView): + permission_classes = [IsAuthenticated] + + def patch(self, request, pk): + from events.models import Event + from django.shortcuts import get_object_or_404 + e = get_object_or_404(Event, pk=pk) + + field_map = { + 'title': 'title', + 'name': 'name', + 'description': 'description', + 'venueName': 'venue_name', + 'place': 'place', + 'district': 'district', + 'state': 'state', + 'pincode': 'pincode', + 'importantInformation': 'important_information', + 'source': 'source', + 'cancelledReason': 'cancelled_reason', + 'outsideEventUrl': 'outside_event_url', + } + + bool_fields = { + 'isBookable': 'is_bookable', + 'isFeatured': 'is_featured', + 'isTopEvent': 'is_top_event', + 'allYearEvent': 'all_year_event', + } + + updated_fields = [] + for api_key, model_field in field_map.items(): + if api_key in request.data: + setattr(e, model_field, request.data[api_key] or '') + updated_fields.append(model_field) + + for api_key, model_field in bool_fields.items(): + if api_key in request.data: + setattr(e, model_field, bool(request.data[api_key])) + updated_fields.append(model_field) + + # Handle status + if 'status' in request.data: + reverse_map = {v: k for k, v in _EVENT_STATUS_MAP.items()} + backend_status = reverse_map.get(request.data['status'], request.data['status']) + e.event_status = backend_status + updated_fields.append('event_status') + + if updated_fields: + e.save(update_fields=updated_fields) + + # Re-fetch with relations for response + e = Event.objects.select_related('partner').prefetch_related('eventimages_set').get(pk=pk) + return Response(_serialize_event_detail(e)) class EventModerationView(APIView): @@ -970,3 +1075,223 @@ class ReviewDeleteView(APIView): review = get_object_or_404(Review, pk=pk) review.delete() return Response(status=204) + + +# --- Payment Gateway Settings --- + +def _serialize_gateway(gw, include_secret=False): + data = { + 'id': gw.pk, + 'payment_gateway_id': gw.payment_gateway_id, + 'name': gw.payment_gateway_name, + 'description': gw.payment_gateway_description, + 'url': gw.payment_gateway_url, + 'api_key': gw.payment_gateway_api_key, + 'api_url': gw.payment_gateway_api_url, + 'api_version': gw.payment_gateway_api_version, + 'api_method': gw.payment_gateway_api_method, + 'is_active': gw.is_active, + 'gateway_priority': gw.gateway_priority, + 'created_date': gw.created_date.isoformat() if gw.created_date else None, + 'updated_date': gw.updated_date.isoformat() if gw.updated_date else None, + 'logo': gw.payment_gateway_logo.url if gw.payment_gateway_logo else None, + } + if include_secret: + data['api_secret'] = gw.payment_gateway_api_secret + return data + + +class ActivePaymentGatewayView(APIView): + permission_classes = [AllowAny] + + def get(self, request): + from banking_operations.models import PaymentGateway + gateway = PaymentGateway.objects.filter(is_active=True).order_by('-gateway_priority', '-id').first() + if not gateway: + return Response({'status': 'error', 'message': 'No active payment gateway configured.'}, status=404) + return Response({ + 'status': 'success', + 'gateway': { + 'name': gateway.payment_gateway_name, + 'key_id': gateway.payment_gateway_api_key, + 'currency': 'INR', + } + }) + + +class PaymentGatewaySettingsView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, pk=None): + from banking_operations.models import PaymentGateway + gateways = PaymentGateway.objects.all().order_by('-gateway_priority', '-id') + return Response({ + 'status': 'success', + 'payment_gateways': [_serialize_gateway(g, include_secret=True) for g in gateways] + }) + + def post(self, request, pk=None): + from banking_operations.models import PaymentGateway + import uuid + d = request.data + required = ['name', 'api_key', 'api_secret'] + missing = [f for f in required if not d.get(f)] + if missing: + return Response({'status': 'error', 'message': 'Missing fields: {}'.format(missing)}, status=400) + gw = PaymentGateway.objects.create( + payment_gateway_id=str(uuid.uuid4().hex[:10]).upper(), + payment_gateway_name=d['name'], + payment_gateway_description=d.get('description', ''), + payment_gateway_url=d.get('url', ''), + payment_gateway_api_key=d['api_key'], + payment_gateway_api_secret=d['api_secret'], + payment_gateway_api_url=d.get('api_url', ''), + payment_gateway_api_version=d.get('api_version', 'v1'), + payment_gateway_api_method=d.get('api_method', 'POST'), + is_active=d.get('is_active', True), + gateway_priority=int(d.get('gateway_priority', 0)), + ) + return Response({'status': 'success', 'payment_gateway': _serialize_gateway(gw, include_secret=True)}, status=201) + + def patch(self, request, pk=None): + from banking_operations.models import PaymentGateway + from django.shortcuts import get_object_or_404 + gw = get_object_or_404(PaymentGateway, pk=pk) + d = request.data + field_map = { + 'name': 'payment_gateway_name', + 'description': 'payment_gateway_description', + 'url': 'payment_gateway_url', + 'api_key': 'payment_gateway_api_key', + 'api_secret': 'payment_gateway_api_secret', + 'api_url': 'payment_gateway_api_url', + 'api_version': 'payment_gateway_api_version', + 'api_method': 'payment_gateway_api_method', + 'is_active': 'is_active', + 'gateway_priority': 'gateway_priority', + } + for client_field, model_field in field_map.items(): + if client_field in d: + setattr(gw, model_field, d[client_field]) + gw.save() + return Response({'status': 'success', 'payment_gateway': _serialize_gateway(gw, include_secret=True)}) + + def delete(self, request, pk=None): + from banking_operations.models import PaymentGateway + from django.shortcuts import get_object_or_404 + gw = get_object_or_404(PaymentGateway, pk=pk) + gw.delete() + return Response({'status': 'success'}, status=200) + + +class EventCreateView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + from events.models import Event, EventType + + data = request.data + + # Required fields + title = (data.get('title') or '').strip() + name = (data.get('name') or title).strip() + if not title: + return Response({'error': 'Title is required'}, status=400) + + # Get event_type (required FK) + event_type_id = data.get('eventType') + if not event_type_id: + return Response({'error': 'Event type is required'}, status=400) + + try: + event_type = EventType.objects.get(id=event_type_id) + except EventType.DoesNotExist: + return Response({'error': 'Invalid event type'}, status=400) + + # Build the event + event = Event( + title=title, + name=name, + description=data.get('description', ''), + event_type=event_type, + event_status=data.get('eventStatus', 'pending'), + venue_name=data.get('venueName', ''), + place=data.get('place', ''), + district=data.get('district', ''), + state=data.get('state', ''), + pincode=data.get('pincode', ''), + latitude=data.get('latitude', 0), + longitude=data.get('longitude', 0), + is_bookable=data.get('isBookable', False), + is_featured=data.get('isFeatured', False), + is_top_event=data.get('isTopEvent', False), + all_year_event=data.get('allYearEvent', False), + source=data.get('source', 'official'), + important_information=data.get('importantInformation', ''), + cancelled_reason=data.get('cancelledReason', 'NA'), + outside_event_url=data.get('outsideEventUrl', 'NA'), + is_eventify_event=data.get('isEventifyEvent', True), + ) + + # Optional dates/times + if data.get('startDate'): + event.start_date = data['startDate'] + if data.get('endDate'): + event.end_date = data['endDate'] + if data.get('startTime'): + event.start_time = data['startTime'] + if data.get('endTime'): + event.end_time = data['endTime'] + + # Optional partner + partner_id = data.get('partnerId') + if partner_id: + try: + from partners.models import Partner + event.partner = Partner.objects.get(id=partner_id) + event.is_partner_event = True + except Exception: + pass + + event.save() + + return Response(_serialize_event_detail(event), status=201) + + +class EventTypesView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + from events.models import EventType + types = EventType.objects.all().order_by('id') + return Response([ + {'id': t.id, 'name': t.event_type} + for t in types + ]) + + +class EventPrimaryImageView(APIView): + permission_classes = [IsAuthenticated] + + def patch(self, request, pk): + from events.models import Event, EventImages + try: + event = Event.objects.get(pk=pk) + except Event.DoesNotExist: + return Response({"error": "Event not found"}, status=404) + + image_id = request.data.get("image_id") + if not image_id: + return Response({"error": "image_id is required"}, status=400) + + try: + img = EventImages.objects.get(pk=image_id, event=event) + except EventImages.DoesNotExist: + return Response({"error": "Image not found for this event"}, status=404) + + # Clear all primary flags for this event, then set the selected one + EventImages.objects.filter(event=event).update(is_primary=False) + img.is_primary = True + img.save() + + return Response({"success": True, "primaryImageId": image_id}) diff --git a/eventify/settings.py b/eventify/settings.py index 7a098ed..b3adf76 100644 --- a/eventify/settings.py +++ b/eventify/settings.py @@ -3,7 +3,7 @@ from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent -SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'change-me-in-production') +SECRET_KEY = os.environ['DJANGO_SECRET_KEY'] # DEBUG = os.environ.get('DJANGO_DEBUG', 'False') == 'True' # @@ -12,7 +12,13 @@ SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'change-me-in-production') DEBUG = False ALLOWED_HOSTS = [ - '*' + 'db.eventifyplus.com', + 'uat.eventifyplus.com', + 'backend.eventifyplus.com', + 'admin.eventifyplus.com', + 'app.eventifyplus.com', + 'localhost', + '127.0.0.1', ] INSTALLED_APPS = [ @@ -58,6 +64,9 @@ MIDDLEWARE = [ ] CORS_ALLOWED_ORIGINS = [ + "https://app.eventifyplus.com", + "https://admin.eventifyplus.com", + "https://uat.eventifyplus.com", "http://localhost:5178", "http://localhost:5179", "http://localhost:5173", @@ -107,7 +116,6 @@ DATABASES = { # 'ENGINE': 'django.db.backends.postgresql', # 'NAME': 'eventify_uat_db', # your DB name # 'USER': 'eventify_uat', # your DB user -# 'PASSWORD': 'eventifyplus@!@#$', # your DB password # 'HOST': '0.0.0.0', # or IP/domain # 'PORT': '5440', # default PostgreSQL port # } @@ -148,6 +156,8 @@ SUMMERNOTE_THEME = 'bs5' # Reverse proxy / CSRF fix CSRF_TRUSTED_ORIGINS = [ + 'https://app.eventifyplus.com', + 'https://admin.eventifyplus.com', 'https://db.eventifyplus.com', 'https://uat.eventifyplus.com', 'https://test.eventifyplus.com', @@ -170,8 +180,9 @@ REST_FRAMEWORK = { } SIMPLE_JWT = { - 'ACCESS_TOKEN_LIFETIME': timedelta(days=1), + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30), # Reduced from 1 day for security 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), + 'ROTATE_REFRESH_TOKENS': True, 'AUTH_HEADER_TYPES': ('Bearer',), 'USER_ID_FIELD': 'id', 'USER_ID_CLAIM': 'user_id',