AdminLoginView previously accepted any valid credential regardless of role. partner_manager / partner / partner_staff / partner_customer / customer accounts could obtain admin JWTs and land on admin.eventifyplus.com, where protected pages would render generic "not found" empty states. Now returns 403 for those roles unless the user is a superuser or has an attached StaffProfile. Writes an auth.admin_login_failed audit row with reason=non_admin_role. Closes gap reported for novakopro@gmail.com on /partners/3.
20 KiB
20 KiB
Changelog
All notable changes to the Eventify Backend are documented here. Format follows Keep a Changelog, versioning follows Semantic Versioning.
[1.14.1] — 2026-04-21
Security
AdminLoginViewnow rejects non-admin roles — users withrolein{customer, partner, partner_manager, partner_staff, partner_customer}can no longer obtain an admin JWT viaPOST /api/v1/auth/login/. Returns HTTP 403 with{'error': 'This account is not authorized for the admin dashboard.'}and writes anauth.admin_login_failedaudit row withreason: 'non_admin_role'. Superusers and any user with an attachedStaffProfileremain allowed regardless of role, so existing admin staff are unaffected. Closes the gap where partner_manager accounts (e.g.novakopro@gmail.com) could log intoadmin.eventifyplus.comand hit protected routes
[1.14.0] — 2026-04-21
Added
- Module-level RBAC scopes for Reviews, Contributions, Leads, Audit Log —
SCOPE_DEFINITIONSinadmin_api/views.pyextended 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
- Reviews:
NotificationScheduleaudit emissions inadmin_api/views.py—NotificationScheduleListView.postandNotificationScheduleDetailView.patch/.deletenow writenotification.schedule.created/.updated/.deletedAuditLogrows. Update emits only when at least one field actually changed. Delete capturesname/notification_type/cron_expressionbefore the row is deleted so the audit trail survives the deletion
Fixed
StaffProfile.get_allowed_modules()inadmin_api/models.py—SCOPE_TO_MODULEwas missing the'reviews': 'reviews'entry, so staff grantedreviews.*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 anAuditLogrow:
| 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_loghelper — optionaluser=Nonekwarg soAdminLoginViewcan 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 torequest.user).admin_api/tests.py—AuthAuditEmissionTests(login success + failed login) andEventCrudAuditTests(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 sametransaction.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 capturereason,previous_status,new_statusEventModerationView(PATCH /api/v1/events/<id>/moderate/) —event.approved,event.rejected,event.flagged,event.featured,event.unfeatured; details includereason,partner_id,previous_status/new_status,previous_is_featured/new_is_featuredReviewModerationView(PATCH /api/v1/reviews/<id>/moderate/) —review.approved,review.rejected,review.edited; details includereject_reason,edited_textflag,original_texton editsPartnerKYCReviewView(POST /api/v1/partners/<id>/kyc/review/) —partner.kyc.approved,partner.kyc.rejected,partner.kyc.requested_info(newrequested_infodecision leaves compliance state intact and only records the info request)
GET /api/v1/rbac/audit-log/metrics/—AuditLogMetricsViewreturnstotal,today,week,distinct_users, and aby_action_groupbreakdown (create/update/delete/moderate/auth/other). Cached 60 s under keyadmin_api:audit_log:metrics:v1; pass?nocache=1to bypass (useful from the Django shell during incident response)GET /api/v1/rbac/audit-log/— free-textsearchparameter (Q-filter overaction,target_type,target_id,user__username,user__email);page_sizenow bounded to[1, 200]with defensive fallback to defaults on non-integer inputaccounts.User.ALL_MODULES— appendedaudit-log;StaffProfile.get_allowed_modules()adds'audit'→'audit-log'toSCOPE_TO_MODULEso scope-based staff resolve the module correctlyadmin_api/migrations/0005_auditlog_indexes.py— composite indexes(action, -created_at)and(target_type, target_id)onAuditLogto keep the /audit-log page fast past ~10k rows; reversible via Django's defaultRemoveIndexreverse opadmin_api/tests.py—AuditLogListViewTests,AuditLogMetricsViewTests,UserStatusAuditEmissionTestscovering 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.py—WorldlineClient: HMAC-SHA256 signed requests,create_hosted_checkout(),get_hosted_checkout_status(),verify_webhook_signature()views.py—POST /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 imagesWorldlineOrdermodel inbanking_operations/models.py— tracks each hosted-checkout session (hosted_checkout_id, reference_id, status, raw_response, webhook_payload)
Booking.payment_statusfield —pending / paid / failed / cancelled(defaultpending); migrationbookings/0002_booking_payment_statusbanking_operations/services.py::transaction_initiate— implemented (was a stub); calls Worldline API, createsWorldlineOrder, returnspayment_urlback toCheckoutAPI- 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
- User adds tickets to cart →
POST /api/bookings/checkout/creates Bookings + callstransaction_initiate transaction_initiatecreatesWorldlineOrder+ calls Worldline → returns redirect URL- Frontend redirects user to Worldline hosted checkout page
- After payment, Worldline redirects to
WORLDLINE_RETURN_URL(app.eventifyplus.com/booking/confirm?hostedCheckoutId=...) - SPA calls
POST /api/payments/verify/— checks local status; if still pending, polls Worldline API directly - Worldline webhook fires
POST /api/payments/webhook/→ generates Tickets (one per quantity), marks Bookingpaid, sends confirmation email with QR codes - Partner scans QR code at event → existing
POST /api/bookings/check-in/marksTicket.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
GoogleLoginViewaudience-check fix (POST /api/user/google-login/) — CRITICAL security patchverify_oauth2_token(token, google_requests.Request())was called without the thirdaudienceargument, 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 whoseaudclaim matches our registered Client ID are now accepted - Added fail-closed guard: if
settings.GOOGLE_CLIENT_IDis empty the view returns HTTP 503 instead of silently accepting all tokens
Changed
- Removed Clerk scaffolding — the
@clerk/reactbroker 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()frommobile_api/views/user.py - Removed
path('user/clerk-login/', ...)frommobile_api/urls.py - Removed
CLERK_JWKS_URL/CLERK_ISSUER/CLERK_SECRET_KEYfromeventify/settings.py; replaced withGOOGLE_CLIENT_ID = os.environ.get('GOOGLE_CLIENT_ID', '') - Removed
PyJWT[crypto]>=2.8.0andrequests>=2.31.0fromrequirements.txt+requirements-docker.txt(no longer needed;google-auth>=2.0.0handles verification)
- Removed
Added
- Settings:
GOOGLE_CLIENT_ID = os.environ.get('GOOGLE_CLIENT_ID', '')ineventify/settings.py - Tests:
mobile_api/tests.py::GoogleLoginViewTests— 4 cases: valid token creates user (audience arg verified), missingid_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_userare unchanged. - Deploy requirement: set
GOOGLE_CLIENT_IDin the Django container.envbefore deploying — without it the view returns 503 (fail-closed by design).
[1.9.0] — 2026-04-07
Added
- Lead Manager — new
Leadmodel inadmin_apifor 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_leadwith 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 statusGET /api/v1/leads/— paginated list with filters (status, priority, source, search, date_from, date_to)GET /api/v1/leads/<id>/— single lead detailPATCH /api/v1/leads/<id>/update/— update status, priority, assigned_to, notes
- RBAC:
leadsadded toALL_MODULES,get_allowed_modules(), andStaffProfile.SCOPE_TO_MODULE
[1.8.3] — 2026-04-06
Fixed
TopEventsAPInow works without authentication —POST /api/events/top-events/hadAllowAnypermission but still calledvalidate_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 (wasis_top_event=Trueonly) - Added
event_type_namefield resolution:e.event_type.event_type if e.event_type else ''—model_to_dict()only returns the FK integer
- Removed
[1.8.2] — 2026-04-06
Fixed
FeaturedEventsAPInow returnsevent_type_namestring —model_to_dict()serialises theevent_typeFK as an integer ID; the hero slider frontend readsev.event_type_nameto display the category badge, which was alwaysnull- Added
data_dict['event_type_name'] = e.event_type.event_type if e.event_type else ''aftermodel_to_dict(e)to resolve the FK to its human-readable name (e.g."Festivals") - No frontend changes required —
fetchHeroSlides()already falls back toev.event_type_name
- Added
[1.8.1] — 2026-04-06
Fixed
FeaturedEventsAPInow works without authentication —POST /api/events/featured-events/hadAllowAnypermission but still calledvalidate_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'(wasis_featured=Trueonly) to matchConsumerFeaturedEventsViewbehaviour and avoid returning draft/cancelled events - Root cause: host Nginx routes
/api/→eventify-backendcontainer (port 3001), noteventify-django(port 8085); thevalidate_token_and_get_usergate in this container was silently blocking all hero slider requests
- Removed the
[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_atDateTimeField on User model (migration0013_user_district_changed_at) — nullable, no backfill; NULL means "eligible to change immediately"VALID_DISTRICTSconstant (14 Kerala districts) inaccounts/models.pyfor server-side validationWebRegisterFormnow accepts optionaldistrictfield; stampsdistrict_changed_aton valid selection during signupUpdateProfileViewenforces 183-day (~6 months) cooldown — rejects district changes within the window with a human-readable "Next change: {date}" errordistrict_changed_atincluded in all relevant API responses:LoginView,WebRegisterView,StatusView,UpdateProfileViewStatusViewnow also returnsdistrictfield (was previously missing)
[1.6.2] — 2026-04-03
Security
- Internal exceptions no longer exposed to API callers — all 15
except Exception as eblocks acrossmobile_api/views/user.pyandmobile_api/views/events.pynow log the real error viaeventify_loggerand 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 StatusViewandUpdateProfileViewwere also missinglog(...)calls entirely — addedfrom eventify_logger.services import logimport added toevents.py(was absent)
- Affected views:
[1.6.1] — 2026-04-03
Added
eventify_idinStatusViewresponse (/api/user/status/) — consumer app uses this to refresh the Eventify ID badge (EVT-XXXXXXXX) for sessions that pre-date theeventify_idlogin fieldaccountsmigration0012_user_eventify_iddeployed 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-XXXXXXXXformat)- New
eventify_idfield onUsermodel —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 usingsecrets.choice() - Migration
0012_user_eventify_id: add nullable → backfill all existing users → make non-null
- New
eventify_idexposed inaccounts/api.py→_partner_user_to_dict()fields listeventify_idexposed inpartner/api.py→_user_to_dict()fields listeventify_idexposed inmobile_api/views/user.py→LoginViewresponse (populateslocalStorage.event_user.eventify_id)eventifyIdexposed inadmin_api/views.py→_serialize_user()(camelCase for direct TypeScript compatibility)- Server-side search in
UserListViewnow also filters oneventify_id__icontains - Synced migration
0011_user_allowed_modules_alter_user_id(pulled from server, was missing from local repo)
Changed
accounts/models.py: mergedallowed_modulesfield +get_allowed_modules()+ALL_MODULESconstant from server (previously only existed on server)
[1.5.0] — 2026-03-31
Added
allowed_modulesTextField onUsermodel — comma-separated module slug access controlget_allowed_modules()method onUser— returns list of accessible modules based on role or explicit listALL_MODULESclass 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
idfield changed fromAutoFieldtoBigAutoField(migration0010_alter_user_id)
[1.2.0] — 2026-03-10
Added
partnerForeignKey onUsermodel linking users to partners (migration0009_user_partner)- Profile picture upload support (
ImageField) withdefault.pngfallback (migration0006–0007)
[1.1.0] — 2026-02-28
Added
- Location fields on
User:pincode,district,state,country,place,latitude,longitude - Custom
UserManagerfor programmatic user creation
[1.0.0] — 2026-03-01
Added
- Initial Django project with custom
Usermodel extendingAbstractUser - 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