Security audit follow-up: httpOnly cookies, ticket reservations, MongoDB keyfile

Deferred findings from the March 27 security audit, plus a bug fix:

MongoDB keyfile (bug fix):
- Generate replica.key on first boot via entrypoint script
- Fixes crash from --auth + --keyFile without an existing keyfile
- Applied to docker-compose.yml, docker-compose.prod.yml, CCP template

I7 — Ticket overselling prevention (reservation pattern):
- Add reservedCount field to TicketTier schema
- Atomically increment reservedCount inside transaction on checkout
- Release reservation on checkout.session.completed (webhook)
- Release reservation on checkout.session.expired (webhook)
- Include reservedCount in availability calculations

I17 — Move refresh token to httpOnly cookie:
- Server sets httpOnly SameSite=Strict cookie on login/register/refresh
- Cookie scoped to /api/auth path, secure in production
- Refresh/logout endpoints read from cookie (with body fallback for compat)
- Frontend no longer stores refreshToken in localStorage
- Auth store simplified: removed refreshToken from state + persistence
- API interceptor uses withCredentials:true for automatic cookie sending
- Updated media-api, media-public-api, QuickJoinPage, volunteer-invite
- Renamed getTokens → getAccessToken across all media components
- Install cookie-parser middleware

L2 — FeatureGate loading state:
- Show Skeleton instead of children while settings are loading
- Prevents briefly exposing disabled feature pages

Bunker Admin
This commit is contained in:
2026-03-27 09:20:26 -06:00
parent 82a66a97d0
commit b215cda018
25 changed files with 201 additions and 105 deletions

View File

@@ -4830,6 +4830,7 @@ model TicketTier {
minDonationCAD Int? @map("min_donation_cad") // In cents
maxQuantity Int? @map("max_quantity")
soldCount Int @default(0) @map("sold_count")
reservedCount Int @default(0) @map("reserved_count") // Pending checkout sessions
maxPerOrder Int @default(10) @map("max_per_order")
salesStartAt DateTime? @map("sales_start_at")
salesEndAt DateTime? @map("sales_end_at")