The ics library's url validator requires a scheme (https://...), but
production's FRONTEND_URL/APP_BASE_URL was configured without one, so
createEvent() rejected every request before generating a .ics file.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAwumFeBwx6XSsQXp8Nfqt
createEvent never called logAdminAction, even though 1.10.0 already
listed event_created as a filterable action on the audit-log page —
only updateEvent/deleteEvent actually logged. Event creation is now
logged the same way, at every return path including the legacy
pre-migration retry branches.
Also adds a "Back to dashboard" link to Admin -> Audit log, matching
the existing back-link pattern on the Cashup page.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSWFWQsjTc9GyffPiXEDQT
Path traversal (CWE-22/CWE-73): event-image, branding (logo/favicon),
and event-attachment uploads built the saved filename from the
client-supplied original filename with no sanitization, and multer's
diskStorage joins that straight into the destination path. A crafted
filename containing `../` sequences could write the uploaded file
anywhere the server process has write access — reachable by any
supervisor-level account, and briefly pre-auth via the branding
uploads during initial /setup. Filenames are now always server-
generated (random bytes + validated extension); the original name is
kept only as display metadata.
Dependencies: express-rate-limit was declared only at the repo root
despite being required directly by backend/src/index.js, so a plain
`cd backend && npm install` (per the deployment doc) would never
install it — moved it into backend/package.json. Bumped next off a
version affected by a critical unauthenticated RCE (React Flight
protocol) and switched it from an exact pin to a caret range so future
patches install automatically. Bumped multer/nodemailer/jsonwebtoken/
uuid to patched versions, with an override forcing the vulnerable
nested uuid inside exceljs and the vulnerable postcss bundled inside
next to the patched versions too. `npm audit` is now clean (0
vulnerabilities) across root, backend, and frontend.
Hardening: jwt.verify() now pins algorithms: ['HS256'] instead of
trusting the token header; /uploads now serves with a restrictive CSP
and X-Content-Type-Options: nosniff so an uploaded SVG containing
<script> can't execute if opened directly.
Verified: backend's Jest suite passes, the backend boots and serves
real requests on the bumped deps, and `next build` compiles/type-
checks cleanly on the bumped frontend deps.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSWFWQsjTc9GyffPiXEDQT
Six site improvements picked from a "what could be better" review, plus a Jest
test suite covering the two areas with the trickiest money-handling history
in this project (early-bird pricing tranches, donation-leg accounting):
- "Add to calendar" .ics download on event pages and in confirmation emails
- sitemap.xml, robots.txt, and Open Graph/Twitter metadata for public pages
- Sentry error monitoring (backend + frontend), a no-op until SENTRY_DSN is set
- Nightly local pg_dump backups with a Site Settings tab to browse/trigger/download
- Admin audit trail for refunds, donations, manual registrations, event and
settings changes, and staff-initiated cancellations
- Jest tests reproducing and guarding against the 1.8.0 tranche-pricing bug
and the 1.4.2 donation-balance-inflation bug
Wallet passes (Google/Apple) were scoped out of this round — Apple Wallet
needs a paid Apple Developer account the project doesn't have yet, and the
user preferred shipping both together later rather than Google alone now.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The public [redirectUrl] catch-all route (and its backend counterpart,
GET /api/events/by-alias/:redirectUrl) matched any unmatched top-level
path, so routine bot/scanner traffic (/wp-login.php, /.env, etc.) was
firing a live database query on every hit. That traffic pattern looks
like the cause of the P1017 "server has closed the connection" storms
and OOM crashes seen from v1.7 onward. Both now reject anything that
isn't a plausible alias (letters/numbers/hyphens/underscores) before
touching Prisma.
Also adds a max_memory_restart safety net to PM2 for both processes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Events and the organisation profile now have an address, with a
"Directions" link and an embedded Google Maps view (no API key
required) shown on event pages, event cards, and the Contact page.
New events default their location to the org's configured address.
- Registration confirmations attach an invoice PDF (itemized breakdown,
early-bird discount, balance due, Yoco pay-now link/QR) whenever a
balance is outstanding; payment/donation confirmations attach a
payment receipt PDF. Sent as an email attachment and, over WhatsApp,
as the PDF itself with the existing message as its caption.
- Users can also (re)send either document on demand: an "Invoice"
button on the registration detail popup, and a "Receipt" button next
to each payment there and on the Payment history page, each opening
an Email/WhatsApp choice popup, via two new endpoints restricted to
the registration/payment's own owner.
- Fix: editing an event option's early-bird tiers deleted and
recreated every tier for that option with brand-new ids, silently
severing the appliedTierId link on all historical purchases (losing
early-bird attribution and undercounting stock-limit usage) even for
tiers the admin didn't touch. Tiers are now upserted by id.
- Update the "My Events" help content and the API docs index for the
new endpoints.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Early-bird pricing: RegistrationOption now tracks each purchase as a
separate price tranche instead of overwriting a single price/quantity
on repeat purchases, so buying more tickets after a tier expires no
longer re-prices tickets already bought at the old price. Stock-limit
checks, total-due calculation, and the Finance report's revenue-by-
option are all tranche-aware; pages that showed one blended price per
line now render/total each tranche. Viewing a pending/partially-paid
registration (dashboard, detail page, or an event's registration
list) now refreshes stale pricing on the spot instead of only at
payment time.
- Fixed the "(early bird)" dashboard label incorrectly firing on any
line priced below the base option price (e.g. a plain cheaper
variant) — it now checks the real applied-tier flag.
- Added contact-only events (e.g. baptism): no registration/payment
flow, shown on the public site with a "Contact us" popup instead of
a Register button. Configurable via the admin event wizard.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The scheduled-job store never persisted the channel field, so the
send worker always fell through to its email branch regardless of
what was requested. Also purges sent jobs 24h after sending instead
of keeping them forever, and surfaces who each scheduled job will go
to in the admin "manage scheduled" lists (now correctly filtered per
channel too).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Financial correctness (donation-leg model):
- Donations are no longer mutated when assigned to a registration; assignment now
creates an immutable "leg" record referencing the original donation instead.
- Fixed several places where money was double-counted once a donation was partially
or fully assigned (Payments, Revenue summary, Cashup reconciliation, Finance
report, Profit report, Master Orders, Revenue Detailed).
- Payments now record who recorded them (recordedBy), separate from who they're for.
Cashup:
- Per-user cash denomination counting (optional, any time) replaces the single
event-wide manual entry; the event's cash actual is the live sum of these counts.
- New "Payment accountability by staff member" breakdown across all methods, and a
read-only "Report" tab that opens automatically once an event is closed.
Reports page redesign:
- New shell: sidebar of universal filters (events, date range, past/inactive/closed
toggles), searchable/categorized report grid, and a popup viewer with
Print/Email/Excel/WhatsApp actions plus an in-app Reporting Guide.
- Visual pass: colored stat tiles and bar charts on most reports, matching mockups.
- PDF exports (download/Print/Email/WhatsApp) now share a branded design mirroring
the web report — colored header, stat tiles, bar chart, highlighted totals.
- Excel export now produces a styled .xlsx (via exceljs) instead of a plain CSV.
- Master Orders' "Donations made" table is now included in every export channel.
Bug fixes discovered while testing exports:
- Report emails now go through the shared, DB-configurable mail utility instead of
a one-off transporter that ignored Site Settings SMTP config.
- WhatsApp report sends now surface the actual WAWP API error and auto-recover a
disconnected session, instead of a bare axios status-code message.
Also: Admin-editable notification preference, richer Admin Registrations dashboard,
{{payment.link}} placeholder for Email/WhatsApp Attendees, and background
email/WhatsApp attendee sending.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- GET /api/events/all gains an opt-in excludeClosed=true param, used only by
the kiosk, so closed events no longer show as selectable there while other
admin/supervisor screens that still need to see closed events are unaffected.
- GET /api/users/check-exists now also returns the matched account's name,
email, phone, and notification preference (safe fields only). The kiosk's
existing debounced lookup uses this to autofill whichever fields are still
blank when a visitor enters an email or phone that matches an existing
account, without overwriting anything already typed.