Turnstile bot protection (sign-in, sign-up, password-reset): - Register Better Auth's captcha plugin with the cloudflare-turnstile provider; endpoints listed explicitly rather than relying on defaults. /reset-password is intentionally excluded — it is reached only via a single-use emailed token. - Add an explicit-render Turnstile widget component. Tokens are single-use, so each form resets the challenge after a failed submit; submit stays disabled until a token is held. - Read the site key server-side and pass it down as a prop, so rotating it does not require a rebuild. - Fail fast in production when TURNSTILE_SECRET_KEY is missing, and when a secret is set without a site key (that combination would demand a token no form can produce, locking every user out). - Pass a throwaway secret during `next build` in the Dockerfile, mirroring the existing BETTER_AUTH_SECRET treatment, so image builds don't need it. CSP fixes in middleware (these blocked Turnstile entirely): - Add frame-src for challenges.cloudflare.com. Without it the widget's iframe fell back to default-src 'self' and was blocked outright. - Allow 'unsafe-eval' and websockets in DEVELOPMENT only. `next dev` compiles with eval(), so the strict policy threw EvalError and killed hydration — no client JS ran at all, which also meant form submit handlers never fired. Production policy is unchanged and still strict. Also included (concurrent work in the tree): - Admin organizations pages and lib/admin/orgs. - Episode moderation migration, SEO metadata (sitemap, robots, JSON-LD, OG/Twitter images, manifest), Umami analytics, not-found page. Local dev database: docker-compose.dev.yml provisions Postgres 18 on port 5443 (5432-5442 are in use by other local projects). Note: `npx tsc --noEmit` currently fails in app/(app)/team/page.tsx — an `invitations` prop the component does not accept. This predates the commit and will fail `next build` until fixed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
52 lines
1.9 KiB
TypeScript
52 lines
1.9 KiB
TypeScript
"use client";
|
|
|
|
import Script from "next/script";
|
|
import { ANALYTICS_PROXY_PATH, redactPayload, type UmamiPayload } from "@/lib/analytics";
|
|
|
|
/** Name of the global the tracker's `data-before-send` hook resolves. */
|
|
const BEFORE_SEND = "__umamiBeforeSend";
|
|
|
|
declare global {
|
|
interface Window {
|
|
[BEFORE_SEND]?: (type: string, payload: UmamiPayload) => UmamiPayload;
|
|
}
|
|
}
|
|
|
|
// Registered at module scope rather than in an effect: the tracker reads
|
|
// `window[BEFORE_SEND]` at send time, and this client chunk is evaluated before
|
|
// next/script injects the tag, so there is no window in which an unredacted
|
|
// event could slip out.
|
|
if (typeof window !== "undefined") {
|
|
window[BEFORE_SEND] = (_type, payload) => redactPayload(payload);
|
|
}
|
|
|
|
/**
|
|
* Self-hosted Umami analytics.
|
|
*
|
|
* The tracker and its beacon are both served from this origin via the
|
|
* `/_a` rewrite in next.config.mjs. That matters for more than ad-blockers: the
|
|
* CSP in middleware.ts uses `'strict-dynamic'`, which makes browsers ignore host
|
|
* allowlists in `script-src` entirely — so allowlisting the Umami domain there
|
|
* would not have worked, and `connect-src 'self'` would still have blocked the
|
|
* beacon. Proxying keeps both same-origin and the policy unrelaxed.
|
|
*/
|
|
export function UmamiAnalytics({ websiteId }: { websiteId: string }) {
|
|
return (
|
|
<Script
|
|
src={`${ANALYTICS_PROXY_PATH}/script.js`}
|
|
strategy="afterInteractive"
|
|
data-website-id={websiteId}
|
|
// Point the beacon at the proxied path instead of letting the tracker
|
|
// derive it from its own src.
|
|
data-host-url={ANALYTICS_PROXY_PATH}
|
|
// Drop query strings and fragments at the source. /reset-password carries a
|
|
// live reset token in `?token=` and /sign-in carries `?redirect=`.
|
|
data-exclude-search="true"
|
|
data-exclude-hash="true"
|
|
data-before-send={BEFORE_SEND}
|
|
// Honour the browser's Do Not Track signal.
|
|
data-do-not-track="true"
|
|
/>
|
|
);
|
|
}
|