17 Commits
Author SHA1 Message Date
Leon SerfatyandClaude Opus 5 68c0b1306d fix(csp): blank page on statically prerendered routes
The nonce-based script-src is incompatible with Next's static prerendering.
The nonce is minted per request in middleware, but statically generated HTML
was produced at build time with no nonce on its inline scripts, so on "/" and
the marketing routes every inline hydration script was blocked: the page
painted from SSR HTML, React failed to hydrate, and the tree unmounted to a
blank screen a moment later. Dynamic routes (/sign-in, /dashboard) render per
request, receive the nonce, and worked — which is why this was missed.

Browsers that honour a nonce ignore 'unsafe-inline', so the two cannot be
combined. Drop the nonce and allow inline scripts; the policy still restricts
which origins may serve scripts, and frame-src/connect-src stay strict.
Restoring nonces would require every route to render dynamically.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 12:16:09 -04:00
Leon SerfatyandClaude Opus 5 fc00ad5b32 fix(auth): don't require email verification to sign in
Transactional email only sends when RESEND_API_KEY is set; without it
sendEmail() silently no-ops. Combined with requireEmailVerification: true,
that meant every newly registered account was permanently locked out — the
verification link was never delivered and unverified users could not log in.

Verification mail is still sent on signup when email is configured; it is no
longer a barrier to signing in. Re-enable once transactional email is live.

Note: password reset still depends on email delivery, so users who forget a
password remain stuck until RESEND_API_KEY is configured.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 12:04:19 -04:00
Leon SerfatyandClaude Opus 5 8067980774 fix(deploy): inject Dokploy env into containers via env_file
The compose services declared only STORAGE_DIR and PORT under `environment`,
so DATABASE_URL, BETTER_AUTH_SECRET and the Turnstile keys never reached the
containers: `docker compose` reads the generated .env for ${VAR} interpolation
(build args) but does not pass it into services on its own. `prisma migrate
deploy` therefore started with no DATABASE_URL and both containers exited 1 in
a restart loop.

Add `env_file: [.env]` to web and worker so the Environment tab actually
becomes runtime configuration, as the file's own comment already claimed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 11:57:10 -04:00
Leon SerfatyandClaude Opus 5 0c6b6c5cad fix(deploy): attach compose services to dokploy-network
Dokploy's managed Postgres runs as its own service on the shared
`dokploy-network`. The compose stack only had its own default network, so
web and worker could not resolve the database host — `prisma migrate deploy`
failed on boot and both containers crash-looped.

Join both services to `dokploy-network` while keeping `default` for
web<->worker traffic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 11:52:14 -04:00
Leon SerfatyandClaude Opus 5 3e9ba07175 feat: Cloudflare Turnstile on auth, CSP fixes, admin/SEO/analytics additions
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>
2026-09-07 11:10:55 -04:00
Leon SerfatyandClaude Opus 4.8 35379212fb fix(worker): neutralize server-only at runtime so the tsx worker can boot
The worker runs worker/index.ts under tsx (plain Node) and transitively
imports lib/flags (and other server-only-guarded modules) via the generation
pipeline. `server-only` resolves to its throwing index.js outside Next's RSC
bundler, crash-looping the worker container. Copy the package's empty.js over
index.js in the final image so the runtime import is a no-op; the build-time
client/server guard already ran during `next build`. Web is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:31:38 -04:00
Leon SerfatyandClaude Opus 4.8 328cd62766 fix(docker): use a >=32-char build-time BETTER_AUTH_SECRET placeholder
The security-hardening pass tightened lib/auth/auth.ts to reject secrets
shorter than 32 chars (or known placeholders) in production. next build
evaluates that guard while collecting page data for /api/auth/[...all], so
the old 21-char "build-time-placeholder" now fails the build. Lengthen the
throwaway build-only value to satisfy the guard; the real secret is still
injected at run time and never baked into the image.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:19:56 -04:00
Leon SerfatyandClaude Opus 4.8 19a403b431 fix(storage): serve assets via app routes when MEDIA_PUBLIC_BASE_URL is unset
publicUrl() defaulted MEDIA_BASE to "/media" even with no env set, so cover
art on public share pages pointed at a /media path that only the old nginx
(Plesk) box served. In the containerized Coolify/Docker deploy nothing serves
/media, breaking those images. Return null when MEDIA_PUBLIC_BASE_URL is unset
so callers fall back to the app's own /api/public/.../cover route (the deploy
README already documents assets as app-served). Backward compatible: behavior
is unchanged when the env var is set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:10:52 -04:00
Leon Serfaty 51c541ad22 Security & robustness hardening pass
Cross-cutting input-validation, isolation, and DoS-resistance fixes across
the app, API, billing, queue, and infra layers.

- Runtime validation (zod) for client-supplied admin actions (role/plan/
  limits), series generation index, and all pg-boss queue payloads
- Auth: require email verification before sign-in; reject weak/placeholder/
  short BETTER_AUTH_SECRET in production
- Billing: sanitize Stripe/PayPal errors (log server-side, generic to client);
  race-safe subscription upsert; only count "processed" webhook events as
  handled; verify org membership in getEffectivePlan to block plan escalation
- Series generation: reserve usage up front and refund on failure; bill the
  owning org, not the caller's active org
- Injection defenses: HTML-escape user fields in emails, strip CR/LF from
  subject/recipient, validate ElevenLabs voiceId before URL interpolation
- Media routes: stream off disk instead of buffering whole files; rate-limit
  anonymous public audio/cover endpoints by client IP
2026-06-20 20:59:03 -04:00
Leon SerfatyandClaude Opus 4.8 cd1d6a1a28 Add brand logo + favicon across app surfaces
Ship the Podcast Distribution AI wordmark as real assets and replace the
Mic-tile + text placeholder everywhere it appeared.

- public/logo-dark.png (dark wordmark, for light backgrounds) and
  public/logo-light.png (light wordmark, for dark backgrounds)
- New <Logo> component swaps the two via Tailwind dark: variants, so the
  dark logo shows on all non-themed surfaces (marketing, auth, admin,
  public share) and the light logo only in dark-mode app surfaces
- Wire <Logo> into the marketing header/footer, auth layout, app header
  (default branch only - white-label org logos untouched), admin header
  (+ Admin badge), and the public share page
- Favicon via App Router file convention: app/icon.png + app/apple-icon.png

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:58:37 -04:00
Leon Serfaty 013a3525e5 docs: point deploy git URL at the actual repo (admin/podcastdistributiona) 2026-06-20 20:17:09 -04:00
Leon Serfaty 4fb2645db1 Rebrand PodcastYes -> Podcast Distribution AI (podcastdistributionai.com) 2026-06-20 20:12:43 -04:00
Leon Serfaty 3ad90f076f fix: tolerate empty NEXT_PUBLIC_APP_URL during production build 2026-06-08 04:46:58 -04:00
Leon Serfaty 305278a846 Add comprehensive /features page + allow Unsplash imagery 2026-06-08 04:37:10 -04:00
Leon Serfaty 8138827657 Retarget deployment from Plesk to Dokploy (Docker Compose) 2026-06-07 18:30:53 -04:00
Leon Serfaty f033f00379 Comprehensive admin + user dashboards (production-ready) 2026-06-07 17:54:30 -04:00
Leon Serfaty 155507f21a Initial commit: PodcastYes — AI podcast platform 2026-06-07 03:58:32 -04:00