Three fixes from the earlier review, plus cleanup:
- submissions.service.ts: a newsletter opt-in present but failing
validation (e.g. malformed email) was silently dropped with no signal
to the client - the rest of the submission saved, but the visitor had
no way to know their newsletter signup didn't go through. Added
newsletterDropped to the submit response so the frontend can tell them.
- reports.admin.service.ts: the newsletter summary tracked
total/sent/pending/failed but silently omitted SKIPPED (stub-mode)
signups from any bucket - every current signup showed total>0 with
every bucket reading 0, indistinguishable from "we don't know what
happened". Added a skipped count.
- Consolidated two things duplicated across the module: sendServerError
(reimplemented ~11 times, three of those as identical local copies of
the same function) into feedback.errors.ts, and formatDatetime/
toMysqlDatetime (the same local-time formatting logic under two names,
in csv.service.ts and events.admin.service.ts respectively) into
feedback.dates.ts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New /feedback API domain backed by its own FEEDBACK_DB, mirroring the
Calendar domain's router -> service -> DB pool layering:
- Public endpoints (no auth): eligible-events listing, event config,
submission with honeypot + rate limiting (in-memory + DB backstop).
- Admin endpoints (session-header auth, reusing Calendar's users/sessions
via a swappable feedback.auth.ts boundary): events/songs/questions CRUD,
bulk reorder/assignment, aggregated reporting, CSV export.
- Schema in sql/feedback/001_init.sql (8 tables), applied and verified
against the real FEEDBACK_DB.
- 64 Jest tests covering validation, auth, rate limiting, CSV escaping,
and report aggregation (pure functions, no DB needed).
Includes fixes from a security review: path traversal defense doesn't
apply here (that's the frontend proxy, separate repo), but the
rate-limiter cluster does - recordSubmission now counts every processed
request (not just successful ones), the in-memory Map evicts empty
entries instead of growing unbounded, FEEDBACK_IP_SALT is required at
boot instead of silently degrading to unsalted hashing, and submission
answer/rating arrays are capped and de-duplicated to bound insert
amplification.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>