Acts on the verified findings from the 2026-06 audit (docs/AUDIT_2026-06_
VERIFIED.md). The audit's #1 "middleware never runs" was a false positive
(verified in prod: /hq-command redirects to login). These are the genuine
gaps:
- SEC-04 (HIGH): /api/assets (GET/POST/PUT/DELETE/PATCH) and
/api/branding/favicon (POST) had NO auth. The middleware matcher excludes
/api, so they were world-reachable — anyone could list/upload/rename/
delete CMS files or regenerate the favicon. Added a new getAdminSession()
helper (src/lib/session.ts) and a requireAdmin() guard on every handler.
- DB-01 (HIGH): the ClientUser table (B2B client portal) was defined in the
schema but NEVER created by any migration, and OperationsSignal.clientId +
its FK were missing too. B2B register/login failed at runtime; the
dashboard silently showed 0 clients. New additive migration
20260609120000_add_client_user creates the table, the unique email index,
the clientId column (IF NOT EXISTS), and the FK (duplicate-object guarded).
- SEC-05 (MED-HIGH): operations.ts generateRichEmailHtml() interpolated
item.title/sku/quantity, clientName/Company/Email/Phone and the free-text
message straight into HTML — stored XSS into the team's internal inbox.
Now escaped via escapeHtml/escapeAttr/safeMailto; file links validated to
internal paths only.
- SEC-01 (MED): removed the hardcoded SESSION_SECRET fallback in src/proxy.ts;
it now validates lazily and throws if the secret is missing (mirrors
session.ts), so a runtime env failure can't fall back to a public key.
Verified: next build compiles with SESSION_SECRET unset (Docker parity),
TypeScript clean, prisma schema valid, golden tests 17/17.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Google Analytics integration, off by default and GDPR-compliant for EU:
- src/lib/analytics/gtag.ts: typed event helpers + consent control. Every
function is a safe no-op when NEXT_PUBLIC_GA_ID is unset.
- GoogleAnalytics.tsx: loads gtag.js with Consent Mode v2, all storage
defaulting to "denied". anonymize_ip on, send_page_view off.
- ConsentBanner.tsx: on-brand cookie banner, localized to all 5 locales,
persists choice for one year, flips analytics_storage to granted on accept.
- PageViewTracker.tsx: fires page_view on App Router client navigation
(inside Suspense for useSearchParams).
- Key conversion events wired: ai_consultation_submitted (primary funnel
goal) and ai_chat_opened.
- Consent strings added to messages/{en,it,vec,es,de}.json.
Build plumbing:
- NEXT_PUBLIC_GA_ID inlined at build time via Dockerfile ARG +
docker-compose build.args (NEXT_PUBLIC_* must exist during next build,
not just runtime).
- Nginx CSP extended to allow googletagmanager.com + google-analytics.com.
- env template documents NEXT_PUBLIC_GA_ID (empty = analytics disabled).
Verified: production build inlines the Measurement ID into the client
bundle; site builds cleanly both with and without the ID set.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>