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:
@@ -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'),
|
||||
]
|
||||
@@ -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})
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user