The trigger: PR #458 mounted requireCustomerPortalEnabled which
410'd every /api/customer/* + /api/admin/customers/* request when
the master toggle was off. Some browsers cached that 410 (no
Cache-Control header was set, so heuristic freshness applied —
the wrong default for an authenticated/sensitive surface).
PR #470 reverted the middleware, but a customer whose tab cached
the 410 still saw 410s until they hard-refreshed.
Add noStoreCache middleware and mount it in front of both route
groups. Every response (200, 4xx, 5xx) now carries
`Cache-Control: no-store, no-cache, must-revalidate, private`
plus the HTTP/1.0 Pragma + Expires fallbacks. Any future
transient error from these endpoints can no longer get pinned in
browser or proxy caches and outlive its cause.
Cost is one setHeader per request; applied per route group rather
than globally so static assets + galleries keep their own caching
strategy unchanged.
Includes a dedicated unit test pinning the header set so a future
cleanup pass can't quietly drop it and re-introduce the bug.
4 unit tests pinning the contract of the customer-minted JWT
re-check added in #470:
- via='customer' + customerId, assignment present → next() runs.
- via='customer' + customerId, assignment removed → 403 with
CUSTOMER_ASSIGNMENT_REVOKED code.
- customerId in payload but `via` claim missing → no re-check
(defends against a future refactor accidentally widening the
gate to match every legacy session that happens to carry a
customerId field).
- per-event-password JWT (no via, no customerId) → no
event_customer_assignments query at all (asserted by counting
db() invocations — a regression that quietly added a re-check
here would 403 every guest the moment any unrelated customer
was unassigned from any event).
Same mock pattern as customerAuth.middleware.test.js. The re-check
is the load-bearing piece behind the "Manage galleries" dialog
UX promise — these tests guard it explicitly.
5 new tests covering the diff math (added/removed), the
archived-event filter, the no-op short-circuit when wanted equals
existing, and the type-coercion of the wanted-list input. Mirrors
the existing setAssignmentsForEvent suite shape so the inverse-
direction service function carries equivalent regression coverage.
This function is the writer behind the "Manage galleries" dialog
and the verifyGalleryAccess re-check together form the access-
control story for the whole feature — getting the diff math
wrong here means assignments don't actually revoke, which is the
entire promise of the new UI.
The Dashboard "Recent Activity" widget and the header notifications
dropdown both rendered raw activity-type strings (e.g. the literal
"feature_flags_updated") for any type missing from their lookup
maps — including everything emitted by the recently-added customer
portal (#354), webhooks (#327), API tokens (#322), event types,
event-publish flow, admin user management (#350), and the
feature-flags reorg itself.
Two coordinated changes:
1. Smart formatter for feature_flags_updated. The backend writes
`metadata.changed = { [flagKey]: { from, to } }` on every save.
New formatFeatureFlagsChanged() helper in admin.service.ts reads
that diff and renders:
- 1 change → "Customer Portal enabled"
- N changes → "3 features updated: Customer Portal enabled,
Calendar disabled, Quotes enabled"
Per-flag display labels source from `settings.features.<key>.title`
so they stay in sync with the Features tab. Unknown flag keys
fall through to a humanised version of the key.
2. 33 missing activity types added to BOTH renderers and to the
`admin.activities.*` + `admin.notificationMessages.*` i18n
namespaces across all six locales. Coverage groups: customer
portal (13 types), admin user management (6), webhooks (3),
API tokens (2), event types (4), event publish/logo (3), bulk
delete (1), and assorted post-merge surfaces (4).
The notifications.service.ts switch + admin.service.ts fallback
message map are still duplicated; consolidating them into a
single source of truth is a follow-up worth doing before the
next significant addition. For now both stay in sync via this PR.
en + de hand-translated. nl + pt + ru + fr machine-translated and
flagged for native review per project convention.
Settings → Features showed the customer-portal toggle as "Accounts"
("Konten" in DE, "Comptes" in FR, etc.) — the deeper sub-nav label
inside ClientsLayout — while the prominent menu-bar entry the admin
actually clicks first reads "Clients" / "Kunden". The mismatch was
confusing on first encounter ("which one do I look for?").
Align the Features tab card title and the "Sidebar:" callout with
the menu-bar wording (`navigation.clients`) across all six locales.
The sub-nav inside ClientsLayout keeps its own "Accounts" label —
that one matches the /admin/clients/accounts URL and is correct.
formatBrandingSettings was updated when the BrandingSettings
interface added the footer-overhaul fields (#441 / #440), so the
admin BrandingPage initialised them as empty strings on every load.
Saving any other field then sent the form's empty socials /
promo_markdown / promo_position back to the backend and wiped the
saved values from the DB. The public gallery footer kept rendering
the old values until the next save, which is why the bug appeared
asymmetric (visible to galleries, gone from the admin form).
Add the missing read mappings for the seven branding_* keys so the
form round-trips them correctly.
Reported by @Rekoo-PS in #460 (split out of #447).