Files
eventify_backend/CHANGELOG.md
Sicherhaven 46b391bd51 fix: allow partner portal SSR to reach admin_api (/me/) for impersonation
ALLOWED_HOSTS was missing partner.eventifyplus.com + docker internal
hostnames (eventify-backend, eventify-django). Partner Next.js
server-side authorize() fetch to /api/v1/auth/me/ was rejected with
HTTP 400 DisallowedHost, so admin "Login as Partner" redirected to
/login?error=ImpersonationFailed instead of /dashboard.

Also added `partner` FK to UserSerializer so the /me/ response exposes
the partner id the portal needs to set session.user.partnerId.

Deployed to both eventify-backend and eventify-django containers via
docker cp + HUP.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 10:30:58 +05:30

22 KiB
Raw Permalink Blame History

Changelog

All notable changes to the Eventify Backend are documented here. Format follows Keep a Changelog, versioning follows Semantic Versioning.


[1.14.2] — 2026-04-22

Fixed

  • Admin "Login as Partner" impersonation now completes into /dashboard instead of bouncing back to /login?error=ImpersonationFailed. Two linked issues:
    • ALLOWED_HOSTS (eventify/settings.py) — partner portal's server-side authorize() (Next.js) calls ${BACKEND_API_URL}/api/v1/auth/me/ with BACKEND_API_URL=http://eventify-backend:8000, so the HTTP Host header was eventify-backend — not in the Django allowlist. SecurityMiddleware rejected with HTTP 400 DisallowedHost, authorize() returned null, signIn() failed, and the /impersonate page redirected to the login error. Added partner.eventifyplus.com, eventify-backend, and eventify-django to ALLOWED_HOSTS. Same Host issue was silently breaking regular partner password login too — fixed as a side effect.
    • UserSerializer missing partner field (admin_api/serializers.py) — MeView returned /api/v1/auth/me/ payload with no partner key, so the partner portal's auth.ts set partnerId: "" on the NextAuth session. Downstream dashboard queries that filter by partnerId would then return empty/403. Added partner = PrimaryKeyRelatedField(read_only=True) to the serializer's fields list. Payload now includes "partner": <id>.
  • Deploy: docker cp both files into both eventify-backend and eventify-django containers + kill -HUP 1 on each (per shared admin_api rule).

[1.14.1] — 2026-04-22

Fixed

  • _serialize_review() now returns profile_photo (mobile_api/views/reviews.py) — /api/reviews/list payload was missing the reviewer's photo URL, so the consumer app had no choice but to render DiceBear placeholders for every reviewer regardless of whether they had uploaded a real profile picture
    • Resolves r.reviewer.profile_picture.url when the field is non-empty and the file name is not default.png (the model's placeholder default); returns empty string otherwise so the frontend can fall back cleanly to DiceBear
    • Mirrors the existing pattern in mobile_api/views/user.py (LoginView, StatusView, UpdateProfileView) — same defensive try/except around FK dereference
    • Pure serializer change — no migration, no URL change, no permission change; gunicorn kill -HUP 1 picks it up

[1.14.0] — 2026-04-21

Added

  • Module-level RBAC scopes for Reviews, Contributions, Leads, Audit LogSCOPE_DEFINITIONS in admin_api/views.py extended with 13 new entries so the admin dashboard's Roles & Permissions grid and the new Base Permissions tab can grant/revoke access at module granularity:
    • Reviews: reviews.read, reviews.moderate, reviews.delete
    • Contributions: contributions.read, contributions.approve, contributions.reject, contributions.award
    • Leads: leads.read, leads.write, leads.assign, leads.convert
    • Audit Log: audit.read, audit.export
  • NotificationSchedule audit emissions in admin_api/views.pyNotificationScheduleListView.post and NotificationScheduleDetailView.patch / .delete now write notification.schedule.created / .updated / .deleted AuditLog rows. Update emits only when at least one field actually changed. Delete captures name/notification_type/cron_expression before the row is deleted so the audit trail survives the deletion

Fixed

  • StaffProfile.get_allowed_modules() in admin_api/models.pySCOPE_TO_MODULE was missing the 'reviews': 'reviews' entry, so staff granted reviews.* scopes could not see the Reviews module in their sidebar. Added

[1.13.0] — 2026-04-21

Added

  • Full admin interaction audit coverage_audit_log() calls added to 12 views; every meaningful admin state change now writes an AuditLog row:
View Action slug(s) Notes
AdminLoginView auth.admin_login, auth.admin_login_failed Uses new user= kwarg (anonymous at login time)
PartnerStatusView partner.status_changed Wrapped in transaction.atomic()
PartnerOnboardView partner.onboarded Inside existing transaction.atomic() block
PartnerStaffCreateView partner.staff.created Logged after staff_user.save()
EventCreateView event.created title, partner_id, source in details
EventUpdateView event.updated changed_fields list in details, wrapped in transaction.atomic()
EventDeleteView event.deleted title + partner_id captured BEFORE delete, wrapped in transaction.atomic()
SettlementReleaseView settlement.released prev/new status in details, transaction.atomic()
ReviewDeleteView review.deleted reviewer_user_id + event_id + rating captured BEFORE delete
PaymentGatewaySettingsView gateway.created, gateway.updated, gateway.deleted changed_fields on update
EventPrimaryImageView event.primary_image_changed prev + new primary image id in details
LeadUpdateView lead.updated changed_fields list; only emits if any field was changed
  • _audit_log helper — optional user=None kwarg so AdminLoginView can supply the authenticated user explicitly (request.user is still anonymous at that point in the login flow). All 20+ existing callers are unaffected (no kwarg = falls through to request.user).
  • admin_api/tests.pyAuthAuditEmissionTests (login success + failed login) and EventCrudAuditTests (create/update/delete) bring total test count to 16, all green

[1.12.0] — 2026-04-21

Added

  • Audit coverage for four moderation endpoints — every admin state change now leaves a matching row in AuditLog, written in the same transaction.atomic() block as the state change so the log can never disagree with the database:
    • UserStatusView (PATCH /api/v1/users/<id>/status/) — user.suspended, user.banned, user.reinstated, user.flagged; details capture reason, previous_status, new_status
    • EventModerationView (PATCH /api/v1/events/<id>/moderate/) — event.approved, event.rejected, event.flagged, event.featured, event.unfeatured; details include reason, partner_id, previous_status/new_status, previous_is_featured/new_is_featured
    • ReviewModerationView (PATCH /api/v1/reviews/<id>/moderate/) — review.approved, review.rejected, review.edited; details include reject_reason, edited_text flag, original_text on edits
    • PartnerKYCReviewView (POST /api/v1/partners/<id>/kyc/review/) — partner.kyc.approved, partner.kyc.rejected, partner.kyc.requested_info (new requested_info decision leaves compliance state intact and only records the info request)
  • GET /api/v1/rbac/audit-log/metrics/AuditLogMetricsView returns total, today, week, distinct_users, and a by_action_group breakdown (create/update/delete/moderate/auth/other). Cached 60 s under key admin_api:audit_log:metrics:v1; pass ?nocache=1 to bypass (useful from the Django shell during incident response)
  • GET /api/v1/rbac/audit-log/ — free-text search parameter (Q-filter over action, target_type, target_id, user__username, user__email); page_size now bounded to [1, 200] with defensive fallback to defaults on non-integer input
  • accounts.User.ALL_MODULES — appended audit-log; StaffProfile.get_allowed_modules() adds 'audit''audit-log' to SCOPE_TO_MODULE so scope-based staff resolve the module correctly
  • admin_api/migrations/0005_auditlog_indexes.py — composite indexes (action, -created_at) and (target_type, target_id) on AuditLog to keep the /audit-log page fast past ~10k rows; reversible via Django's default RemoveIndex reverse op
  • admin_api/tests.pyAuditLogListViewTests, AuditLogMetricsViewTests, UserStatusAuditEmissionTests covering list shape, search, pagination bounds, metrics shape + nocache, and audit emission on suspend / ban / reinstate

Deploy notes

Admin users created before this release won't have audit-log in their allowed_modules TextField. Backfill with:

# Django shell
from accounts.models import User
for u in User.objects.filter(role__in=['admin', 'manager']):
    mods = [m.strip() for m in (u.allowed_modules or '').split(',') if m.strip()]
    if 'audit-log' not in mods and mods:  # only touch users with explicit lists
        u.allowed_modules = ','.join(mods + ['audit-log'])
        u.save(update_fields=['allowed_modules'])

Users on the implicit full-access list (empty allowed_modules + admin role) pick up the new module automatically via get_allowed_modules().


[1.11.0] — 2026-04-12

Added

  • Worldline Connect payment integration (banking_operations/worldline/)
    • client.pyWorldlineClient: HMAC-SHA256 signed requests, create_hosted_checkout(), get_hosted_checkout_status(), verify_webhook_signature()
    • views.pyPOST /api/payments/webhook/ (CSRF-exempt, signature-verified Worldline server callback) + POST /api/payments/verify/ (frontend polls on return URL)
    • emails.py — HTML ticket confirmation email with per-ticket QR codes embedded as base64 inline images
    • WorldlineOrder model in banking_operations/models.py — tracks each hosted-checkout session (hosted_checkout_id, reference_id, status, raw_response, webhook_payload)
  • Booking.payment_status field — pending / paid / failed / cancelled (default pending); migration bookings/0002_booking_payment_status
  • banking_operations/services.py::transaction_initiate — implemented (was a stub); calls Worldline API, creates WorldlineOrder, returns payment_url back to CheckoutAPI
  • Settings: WORLDLINE_MERCHANT_ID, WORLDLINE_API_KEY_ID, WORLDLINE_API_SECRET_KEY, WORLDLINE_WEBHOOK_SECRET_KEY, WORLDLINE_API_ENDPOINT (default: sandbox), WORLDLINE_RETURN_URL
  • Requirements: requests>=2.31.0, qrcode[pil]>=7.4.2

Flow

  1. User adds tickets to cart → POST /api/bookings/checkout/ creates Bookings + calls transaction_initiate
  2. transaction_initiate creates WorldlineOrder + calls Worldline → returns redirect URL
  3. Frontend redirects user to Worldline hosted checkout page
  4. After payment, Worldline redirects to WORLDLINE_RETURN_URL (app.eventifyplus.com/booking/confirm?hostedCheckoutId=...)
  5. SPA calls POST /api/payments/verify/ — checks local status; if still pending, polls Worldline API directly
  6. Worldline webhook fires POST /api/payments/webhook/ → generates Tickets (one per quantity), marks Booking paid, sends confirmation email with QR codes
  7. Partner scans QR code at event → existing POST /api/bookings/check-in/ marks Ticket.is_checked_in=True

Deploy requirement

Set in Django container .env:

WORLDLINE_MERCHANT_ID=...
WORLDLINE_API_KEY_ID=...
WORLDLINE_API_SECRET_KEY=...
WORLDLINE_WEBHOOK_SECRET_KEY=...

WORLDLINE_API_ENDPOINT defaults to sandbox — set to production URL when going live.


[1.10.0] — 2026-04-10

Security

  • GoogleLoginView audience-check fix (POST /api/user/google-login/) — CRITICAL security patch
    • verify_oauth2_token(token, google_requests.Request()) was called without the third audience argument, meaning any valid Google-signed ID token from any OAuth client was accepted — token spoofing from external apps was trivially possible
    • Fixed to verify_oauth2_token(token, google_requests.Request(), settings.GOOGLE_CLIENT_ID) — only tokens whose aud claim matches our registered Client ID are now accepted
    • Added fail-closed guard: if settings.GOOGLE_CLIENT_ID is empty the view returns HTTP 503 instead of silently accepting all tokens

Changed

  • Removed Clerk scaffolding — the @clerk/react broker approach added in a prior iteration has been replaced with direct Google Identity Services (GIS) ID-token flow on the frontend. Simpler architecture: one trust boundary instead of three.
    • Removed ClerkLoginView, _clerk_jwks_client, _get_clerk_jwks_client() from mobile_api/views/user.py
    • Removed path('user/clerk-login/', ...) from mobile_api/urls.py
    • Removed CLERK_JWKS_URL / CLERK_ISSUER / CLERK_SECRET_KEY from eventify/settings.py; replaced with GOOGLE_CLIENT_ID = os.environ.get('GOOGLE_CLIENT_ID', '')
    • Removed PyJWT[crypto]>=2.8.0 and requests>=2.31.0 from requirements.txt + requirements-docker.txt (no longer needed; google-auth>=2.0.0 handles verification)

Added

  • Settings: GOOGLE_CLIENT_ID = os.environ.get('GOOGLE_CLIENT_ID', '') in eventify/settings.py
  • Tests: mobile_api/tests.py::GoogleLoginViewTests — 4 cases: valid token creates user (audience arg verified), missing id_token → 400, ValueError (wrong sig / wrong aud) → 401, existing user reuses DRF token

Context

  • The consumer SPA (app.eventifyplus.com) now loads the Google Identity Services script dynamically and POSTs a Google ID token to the existing /api/user/google-login/ endpoint. Django is the sole session authority. localStorage.event_token / event_user are unchanged.
  • Deploy requirement: set GOOGLE_CLIENT_ID in the Django container .env before deploying — without it the view returns 503 (fail-closed by design).

[1.9.0] — 2026-04-07

Added

  • Lead Manager — new Lead model in admin_api for tracking Schedule-a-Call form submissions and sales inquiries
    • Fields: name, email, phone, event_type, message, status (new/contacted/qualified/converted/closed), source (schedule_call/website/manual), priority (low/medium/high), assigned_to (FK User), notes
    • Migration admin_api/0003_lead with indexes on status, priority, created_at, email
  • Consumer endpoint POST /api/leads/schedule-call/ — public (AllowAny, CSRF-exempt) endpoint for the Schedule a Call modal; creates Lead with status=new, source=schedule_call
  • Admin API endpoints (all IsAuthenticated):
    • GET /api/v1/leads/metrics/ — total, new today, counts per status
    • GET /api/v1/leads/ — paginated list with filters (status, priority, source, search, date_from, date_to)
    • GET /api/v1/leads/<id>/ — single lead detail
    • PATCH /api/v1/leads/<id>/update/ — update status, priority, assigned_to, notes
  • RBAC: leads added to ALL_MODULES, get_allowed_modules(), and StaffProfile.SCOPE_TO_MODULE

[1.8.3] — 2026-04-06

Fixed

  • TopEventsAPI now works without authenticationPOST /api/events/top-events/ had AllowAny permission but still called validate_token_and_get_user(), returning {"status":"error","message":"token and username required"} for unauthenticated requests
    • Removed validate_token_and_get_user() call entirely
    • Added event_status='published' filter (was is_top_event=True only)
    • Added event_type_name field resolution: e.event_type.event_type if e.event_type else ''model_to_dict() only returns the FK integer

[1.8.2] — 2026-04-06

Fixed

  • FeaturedEventsAPI now returns event_type_name stringmodel_to_dict() serialises the event_type FK as an integer ID; the hero slider frontend reads ev.event_type_name to display the category badge, which was always null
    • Added data_dict['event_type_name'] = e.event_type.event_type if e.event_type else '' after model_to_dict(e) to resolve the FK to its human-readable name (e.g. "Festivals")
    • No frontend changes required — fetchHeroSlides() already falls back to ev.event_type_name

[1.8.1] — 2026-04-06

Fixed

  • FeaturedEventsAPI now works without authenticationPOST /api/events/featured-events/ had AllowAny permission but still called validate_token_and_get_user(), causing the endpoint to return HTTP 200 + {"status":"error","message":"token and username required"} for unauthenticated requests (e.g. the desktop hero slider)
    • Removed the validate_token_and_get_user() call entirely — the endpoint is public by design and requires no token
    • Also tightened the queryset to event_status='published' (was is_featured=True only) to match ConsumerFeaturedEventsView behaviour and avoid returning draft/cancelled events
    • Root cause: host Nginx routes /api/eventify-backend container (port 3001), not eventify-django (port 8085); the validate_token_and_get_user gate in this container was silently blocking all hero slider requests

[1.8.0] — 2026-04-04

Added

  • BulkUserPublicInfoView (POST /api/user/bulk-public-info/)
    • Internal endpoint for the Node.js gamification server to resolve user details
    • Accepts { emails: [...] } (max 500), returns { users: { email: { display_name, district, eventify_id } } }
    • Used for leaderboard data bridge (syncing user names/districts into gamification DB)
    • CSRF-exempt, returns only public-safe fields (no passwords, tokens, or sensitive PII)

[1.7.0] — 2026-04-04

Added

  • Home District with 6-month cooldown
    • district_changed_at DateTimeField on User model (migration 0013_user_district_changed_at) — nullable, no backfill; NULL means "eligible to change immediately"
    • VALID_DISTRICTS constant (14 Kerala districts) in accounts/models.py for server-side validation
    • WebRegisterForm now accepts optional district field; stamps district_changed_at on valid selection during signup
    • UpdateProfileView enforces 183-day (~6 months) cooldown — rejects district changes within the window with a human-readable "Next change: {date}" error
    • district_changed_at included in all relevant API responses: LoginView, WebRegisterView, StatusView, UpdateProfileView
    • StatusView now also returns district field (was previously missing)

[1.6.2] — 2026-04-03

Security

  • Internal exceptions no longer exposed to API callers — all 15 except Exception as e blocks across mobile_api/views/user.py and mobile_api/views/events.py now log the real error via eventify_logger and return a generic "An unexpected server error occurred." to the caller
    • Affected views: RegisterView, WebRegisterView, LoginView, StatusView, LogoutView, UpdateProfileView, EventTypeAPI, EventListAPI, EventDetailAPI, EventImagesListAPI, EventsByDateAPI, DateSheetAPI, PincodeEventsAPI, FeaturedEventsAPI, TopEventsAPI
    • StatusView and UpdateProfileView were also missing log(...) calls entirely — added
    • from eventify_logger.services import log import added to events.py (was absent)

[1.6.1] — 2026-04-03

Added

  • eventify_id in StatusView response (/api/user/status/) — consumer app uses this to refresh the Eventify ID badge (EVT-XXXXXXXX) for sessions that pre-date the eventify_id login field
  • accounts migration 0012_user_eventify_id deployed to production containers — backfilled all existing users with unique Eventify IDs; previously the migration existed locally but had not been applied in production

[1.6.0] — 2026-04-02

Added

  • Unique Eventify ID system (EVT-XXXXXXXX format)
    • New eventify_id field on User model — CharField(max_length=12, unique=True, editable=False, db_index=True)
    • Charset ABCDEFGHJKLMNPQRSTUVWXYZ23456789 (no ambiguous characters I/O/0/1) giving ~1.78T combinations
    • Auto-generated on first save() via a 10-attempt retry loop using secrets.choice()
    • Migration 0012_user_eventify_id: add nullable → backfill all existing users → make non-null
  • eventify_id exposed in accounts/api.py_partner_user_to_dict() fields list
  • eventify_id exposed in partner/api.py_user_to_dict() fields list
  • eventify_id exposed in mobile_api/views/user.pyLoginView response (populates localStorage.event_user.eventify_id)
  • eventifyId exposed in admin_api/views.py_serialize_user() (camelCase for direct TypeScript compatibility)
  • Server-side search in UserListView now also filters on eventify_id__icontains
  • Synced migration 0011_user_allowed_modules_alter_user_id (pulled from server, was missing from local repo)

Changed

  • accounts/models.py: merged allowed_modules field + get_allowed_modules() + ALL_MODULES constant from server (previously only existed on server)

[1.5.0] — 2026-03-31

Added

  • allowed_modules TextField on User model — comma-separated module slug access control
  • get_allowed_modules() method on User — returns list of accessible modules based on role or explicit list
  • ALL_MODULES class constant listing all platform module slugs
  • Migration 0011_user_allowed_modules_alter_user_id

[1.4.0] — 2026-03-24

Added

  • Partner portal login/logout APIs (accounts/api.py) — PartnerLoginAPI, PartnerLogoutAPI, PartnerMeAPI
  • _partner_user_to_dict() serializer for partner-scoped user data
  • Partner CRUD, KYC review, and user management endpoints in partner/api.py

[1.3.0] — 2026-03-14

Changed

  • User id field changed from AutoField to BigAutoField (migration 0010_alter_user_id)

[1.2.0] — 2026-03-10

Added

  • partner ForeignKey on User model linking users to partners (migration 0009_user_partner)
  • Profile picture upload support (ImageField) with default.png fallback (migration 00060007)

[1.1.0] — 2026-02-28

Added

  • Location fields on User: pincode, district, state, country, place, latitude, longitude
  • Custom UserManager for programmatic user creation

[1.0.0] — 2026-03-01

Added

  • Initial Django project with custom User model extending AbstractUser
  • Role choices: admin, manager, staff, customer, partner, partner_manager, partner_staff, partner_customer
  • JWT authentication via djangorestframework-simplejwt
  • Admin API foundation: auth, dashboard metrics, partners, users, events
  • Docker + Gunicorn + PostgreSQL 16 production setup