Files
eventify_backend/CHANGELOG.md
Sicherhaven 2c60a82704 feat(audit): add Audit Log module — coverage, metrics endpoint, indexes
- UserStatusView, EventModerationView, ReviewModerationView,
  PartnerKYCReviewView: each state change now emits _audit_log()
  inside the same transaction.atomic() block so the log stays
  consistent with DB state on partial failure
- AuditLogMetricsView: GET /api/v1/rbac/audit-log/metrics/ returns
  total/today/week/distinct_users/by_action_group; 60 s cache with
  ?nocache=1 bypass
- AuditLogListView: free-text search (Q over action/target/user),
  page_size bounded to [1, 200]
- accounts.User.ALL_MODULES += 'audit-log';
  StaffProfile.SCOPE_TO_MODULE['audit'] = 'audit-log'
- Migration 0005: composite indexes (action,-created_at) and
  (target_type,target_id) on AuditLog
- admin_api/tests.py: 11 tests covering list shape, search,
  page bounds, metrics shape+nocache, suspend/ban/reinstate
  audit emission

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 12:39:38 +05:30

16 KiB
Raw Blame History

Changelog

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


[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