From b215cda018d5c85e4241cbbdfd2c5876acaa0afe Mon Sep 17 00:00:00 2001 From: bunker-admin Date: Fri, 27 Mar 2026 09:20:26 -0600 Subject: [PATCH] Security audit follow-up: httpOnly cookies, ticket reservations, MongoDB keyfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- admin/src/components/FeatureGate.tsx | 6 +- admin/src/components/media/AlbumCard.tsx | 4 +- .../components/media/AlbumDetailDrawer.tsx | 4 +- admin/src/components/media/PhotoCard.tsx | 4 +- .../src/components/media/PhotoViewerModal.tsx | 4 +- admin/src/components/media/VideoCard.tsx | 4 +- admin/src/components/media/VideoPlayer.tsx | 8 +-- .../src/components/media/VideoViewerModal.tsx | 4 +- admin/src/lib/api.ts | 25 +++----- admin/src/lib/media-api.ts | 18 ++---- admin/src/lib/media-public-api.ts | 2 +- admin/src/pages/public/QuickJoinPage.tsx | 6 +- admin/src/stores/auth.store.ts | 42 ++++--------- api/package-lock.json | 28 +++++++++ api/package.json | 2 + api/prisma/schema.prisma | 1 + api/src/modules/auth/auth.routes.ts | 62 ++++++++++++++++--- api/src/modules/payments/webhook.service.ts | 24 ++++++- .../ticketed-events-public.routes.ts | 38 ++++++++---- .../ticketed-events.service.ts | 2 +- .../volunteer-invite.routes.ts | 10 ++- api/src/server.ts | 2 + .../templates/docker-compose.yml.hbs | 2 +- docker-compose.prod.yml | 2 +- docker-compose.yml | 2 +- 25 files changed, 201 insertions(+), 105 deletions(-) diff --git a/admin/src/components/FeatureGate.tsx b/admin/src/components/FeatureGate.tsx index 3e94d14f..4364cdbe 100644 --- a/admin/src/components/FeatureGate.tsx +++ b/admin/src/components/FeatureGate.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from 'react'; -import { Result, Button } from 'antd'; +import { Result, Button, Skeleton } from 'antd'; import { useNavigate } from 'react-router-dom'; import { SettingOutlined } from '@ant-design/icons'; import { useSettingsStore } from '@/stores/settings.store'; @@ -36,8 +36,8 @@ export default function FeatureGate({ feature, children }: FeatureGateProps) { const isSuperAdmin = hasAnyRole(user, ['SUPER_ADMIN']); const featureName = FEATURE_LABELS[feature] || feature; - // While loading or if settings haven't arrived yet, render children (optimistic) - if (loading || !settings) return <>{children}; + // Show skeleton while settings are loading to prevent briefly showing disabled features + if (loading || !settings) return ; if (settings[feature] === false) { return ( diff --git a/admin/src/components/media/AlbumCard.tsx b/admin/src/components/media/AlbumCard.tsx index d49dc774..e844e787 100644 --- a/admin/src/components/media/AlbumCard.tsx +++ b/admin/src/components/media/AlbumCard.tsx @@ -5,8 +5,8 @@ import type { PhotoAlbum } from '@/types/media'; /** Append JWT access token as query param for src URLs */ function getAuthenticatedUrl(url: string): string { - const { getTokens } = getAuthCallbacks(); - const { accessToken } = getTokens(); + const { getAccessToken } = getAuthCallbacks(); + const accessToken = getAccessToken(); if (!accessToken) return url; const separator = url.includes('?') ? '&' : '?'; return `${url}${separator}token=${accessToken}`; diff --git a/admin/src/components/media/AlbumDetailDrawer.tsx b/admin/src/components/media/AlbumDetailDrawer.tsx index 6d8a4540..a8fda4ff 100644 --- a/admin/src/components/media/AlbumDetailDrawer.tsx +++ b/admin/src/components/media/AlbumDetailDrawer.tsx @@ -12,8 +12,8 @@ import type { PhotoAlbum, PhotoAlbumItem } from '@/types/media'; /** Append JWT access token as query param for src URLs */ function getAuthenticatedUrl(url: string): string { - const { getTokens } = getAuthCallbacks(); - const { accessToken } = getTokens(); + const { getAccessToken } = getAuthCallbacks(); + const accessToken = getAccessToken(); if (!accessToken) return url; const separator = url.includes('?') ? '&' : '?'; return `${url}${separator}token=${accessToken}`; diff --git a/admin/src/components/media/PhotoCard.tsx b/admin/src/components/media/PhotoCard.tsx index 48aaac20..c1d14a92 100644 --- a/admin/src/components/media/PhotoCard.tsx +++ b/admin/src/components/media/PhotoCard.tsx @@ -13,8 +13,8 @@ import type { Photo } from '@/types/media'; /** Append JWT access token as query param for src URLs */ function getAuthenticatedUrl(url: string): string { - const { getTokens } = getAuthCallbacks(); - const { accessToken } = getTokens(); + const { getAccessToken } = getAuthCallbacks(); + const accessToken = getAccessToken(); if (!accessToken) return url; const separator = url.includes('?') ? '&' : '?'; return `${url}${separator}token=${accessToken}`; diff --git a/admin/src/components/media/PhotoViewerModal.tsx b/admin/src/components/media/PhotoViewerModal.tsx index 04188293..1eaad448 100644 --- a/admin/src/components/media/PhotoViewerModal.tsx +++ b/admin/src/components/media/PhotoViewerModal.tsx @@ -5,8 +5,8 @@ import type { Photo } from '@/types/media'; /** Append JWT access token as query param for src URLs */ function getAuthenticatedUrl(url: string): string { - const { getTokens } = getAuthCallbacks(); - const { accessToken } = getTokens(); + const { getAccessToken } = getAuthCallbacks(); + const accessToken = getAccessToken(); if (!accessToken) return url; const separator = url.includes('?') ? '&' : '?'; return `${url}${separator}token=${accessToken}`; diff --git a/admin/src/components/media/VideoCard.tsx b/admin/src/components/media/VideoCard.tsx index 4617169e..1e8ad536 100644 --- a/admin/src/components/media/VideoCard.tsx +++ b/admin/src/components/media/VideoCard.tsx @@ -8,8 +8,8 @@ import ScheduleBadge from './ScheduleBadge'; /** Append JWT access token as query param for /