fix: security audit remediation — Django settings + payment gateway API

- ALLOWED_HOSTS: wildcard replaced with explicit domain list (#15)
- CORS_ALLOWED_ORIGINS: added app.eventifyplus.com (#16)
- CSRF_TRUSTED_ORIGINS: added app.eventifyplus.com (#18)
- JWT ACCESS_TOKEN_LIFETIME: 1 day reduced to 30 minutes (#19)
- ROTATE_REFRESH_TOKENS enabled
- SECRET_KEY: removed unsafe fallback, crash on missing env var
- Added ActivePaymentGatewayView for dynamic gateway config (#1, #5, #20)
- Added PaymentGatewaySettingsView CRUD for admin panel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 12:23:21 +00:00
parent b12f4952b3
commit 5a2752a2de
3 changed files with 356 additions and 11 deletions

View File

@@ -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/<int:pk>/', views.EventDetailView.as_view(), name='event-detail'),
path('events/<int:pk>/update/', views.EventUpdateView.as_view(), name='event-update'),
path('events/<int:pk>/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/<int:pk>/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/<int:pk>/moderate/', views.ReviewModerationView.as_view(), name='review-moderate'),
path('reviews/<int:pk>/', 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/<int:pk>/', views.PaymentGatewaySettingsView.as_view(), name='payment-gateway-detail'),
]

View File

@@ -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})

View File

@@ -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',