Commit Graph
21 Commits
Author SHA1 Message Date
admin 922c2c1aec docs: correct script-src comment — strict-dynamic ignores the added host
Verified live that adding Clerk's frontend API host to script-src does
NOT resolve the CSP violation in modern browsers: with 'strict-dynamic'
present, CSP3 ignores host-based entries in that directive entirely
(Chrome's own violation message confirms this). The console noise is a
Clerk SDK-side loading quirk, not something fixable here without
dropping 'strict-dynamic'. Keeping the host entry only as the same
spec-mandated pre-CSP3 fallback the existing 'unsafe-inline' entry
already relies on, with the comment corrected to say so.
2026-09-01 20:58:07 +02:00
admin 99d9e5813e fix: allow Clerk's frontend API host in CSP script-src
Clerk's clerkMiddleware auto-adds its frontend API host to connect-src
but not script-src, so the initial (unversioned) clerk.browser.js/
ui.browser.js load was blocked by strict-dynamic before the 307 redirect
to the versioned URL — two CSP violations reported to /api/csp-report on
every page that renders Clerk UI, dragging the Best Practices Lighthouse
score down. Derives the host from the publishable key (same approach as
the existing Supabase/GlitchTip hostname helpers) instead of hardcoding it.
2026-09-01 20:23:24 +02:00
admin 9265aa4dc1 fix(security): switch CSP to nonce + strict-dynamic (drop unsafe script-src sources)
Mozilla Observatory's rescan flagged the enforced CSP as "implemented
unsafely" (-20) for unsafe-inline/https:/http: in script-src. Enable
Clerk's strict mode, which drops http:/https: and adds a per-request
nonce + 'strict-dynamic' instead. Clerk forwards the resulting CSP
header (with nonce) on both the response and the downstream request,
which is exactly what Next.js's automatic nonce application reads
during SSR, so framework scripts and page bundles are covered for
free. next-themes' hand-written FOUC-prevention <script> is not
covered by that automatic mechanism, so RootLayout now reads the
x-nonce header and passes it to ThemeProvider explicitly.

'unsafe-inline' stays in the header text as a spec-mandated fallback
for pre-CSP3 browsers; strict-dynamic-aware browsers ignore it.
'unsafe-eval' is kept for now (see prior commit ddbeee7) — untouched
by this change, separate follow-up once confirmed unnecessary.
2026-08-22 14:39:53 +02:00
admin ab1b58d644 feat(security): capture line/column/sample in CSP violation reports
Add 'report-sample' to script-src and capture lineNumber/columnNumber/
sample in /api/csp-report for both report shapes (Reporting API and
legacy report-uri). Previously only sourceFile was captured, which
wasn't enough to pin down the 2026-08-15 eval violations — live repro
via the avatar-crop flow (react-easy-crop) came back clean, so the
next real occurrence needs better diagnostics to actually find it.
2026-08-22 14:14:27 +02:00
admin ddbeee759b fix(security): enforce Content-Security-Policy instead of report-only
Mozilla Observatory docked 25 points for CSP being report-only. The
report-only period since 2026-08-12 came back clean in GlitchTip except
6 isolated script-src eval hits on 2026-08-15 (likely react-easy-crop
on the avatar-crop flow) that never recurred. Keep 'unsafe-eval' in
script-src defensively so an enforced policy can't repeat the earlier
incident where a blocked required resource crashed ClerkProvider
app-wide.
2026-08-22 13:51:55 +02:00
admin 167b9e1a80 feat(seo): add llms.txt, fix middleware blocking .txt static files
Adds public/llms.txt (llms.txt spec: H1 + summary + linked sections)
describing the product for AI agents/crawlers -- usage-focused only, no
internal vendor/infra details.

Also fixes proxy.ts's middleware matcher, which excludes common static
file extensions from Clerk's auth gate but was missing .txt -- every
request to /llms.txt (and any future .txt file, e.g. robots.txt) was
being redirected to the Clerk sign-in page instead of served. No app
route under src/app uses a .txt path, so this only affects genuinely
static files in public/.

SpeedUp-Plan (Gitea #28) -- last remaining Lighthouse finding.
2026-08-20 20:43:25 +02:00
admin 6a61bbcc81 feat(docker): zero-downtime rolling deploy for the 3 replicas
Adds a dependency-free liveness endpoint (GET /api/health), a Docker
healthcheck against it, and docker/rolling-deploy.sh which rebuilds
the shared image once and restarts pawfeed-1/2/3 sequentially, gated
on each becoming healthy before moving to the next. Verified
zero-downtime via continuous curl monitoring through a full run
against production.
2026-08-16 13:17:04 +02:00
admin 9071bf3c9a feat(about): add "Über uns" page with founding story and live stats (Gitea #23)
New public /ueber-uns page — deliberately doesn't preview or link into the
platform itself (no phone mockup, no post previews), just the founding
story plus live member/pet/sponsor counts for transparency. Fully
bilingual via the existing LanguageSwitcher.

- src/trpc/routers/stats.ts: new public stats.getPublicStats procedure,
  Redis-cached (5min TTL), sponsor count includes lapsed subscriptions
  (not just currently-active badges) per discussion on the issue.
- src/proxy.ts: /ueber-uns registered as a public route, exempt from
  maintenance mode (doesn't touch the platform, so no reason to gate it).
- Extracted LandingHeader/LandingFooter from src/app/page.tsx into shared
  components under src/components/landing/ — this page reuses the same
  navbar, and duplicating it risked the responsive header fixes from
  earlier today drifting out of sync between two copies. The "Bald
  verfügbar" placeholder nav link now points at the real page.
- src/lib/stripe.ts: made the Stripe client a lazy singleton instead of
  constructing it at module scope. Eager construction crashed the entire
  tRPC appRouter (and anything importing it, e.g. createTRPCCaller() on
  any public page) whenever STRIPE_SECRET_KEY is unset — invisible until
  now because no public page had used createTRPCCaller() before.
- stats.getPublicStats wraps its Redis cache read/write in .catch() so a
  Redis outage degrades to uncached rather than breaking the page,
  matching the existing pattern in src/lib/maintenance.ts.

Verified locally via dev server + Chrome DevTools MCP emulation (DE/EN,
mobile 375px, desktop) — this is a public unauthenticated page so, unlike
most of today's other fixes, testable outside of Clerk auth.
2026-08-14 18:51:42 +02:00
adminandClaude Sonnet 5 f5f788b38c feat(admin): maintenance mode, registration IP capture, post pinning & trending hashtags (Gitea #10 Phase 4)
Three of the five remaining Phase 4 admin-dashboard items from Gitea #10:

- Maintenance mode: Redis-backed global flag, gate in proxy.ts (exempts
  /p/*, /api/*, legal pages, and sign-in/Clerk routes so a locked-out
  admin can always get back in), SUPER_ADMIN-only dashboard toggle,
  fails open on Redis errors.
- Registration IP: Owner.registrationIp captured defensively at every
  owner.upsert create branch (consume-invite, legal.acknowledge,
  legal.acknowledgeUploadConsent, pets.create, owner.setThemePreference)
  since no single onboarding step is a code-guaranteed first touchpoint
  across all invite/legal-doc states. Datenschutzerklärung updated and
  user-approved.
- Trends & Promotion: Post.pinnedAt field, admin.pinPost/unpinPost
  (mirrors the existing hidePost/unhidePost pattern), explore.getTrending
  now surfaces pinned posts first regardless of the 48h window (with
  explicit nulls:"last" ordering -- Postgres sorts NULL first on DESC by
  default), trending-hashtags panel + pin/unpin button on the admin
  posts page.

Password/2FA-reset admin trigger (item 5) was dropped -- Clerk's own
self-service reset already covers it. Broadcast/system messaging (item 2)
stays deferred pending a shared design session.

Reviewed via /code-review; findings addressed: the registration-IP capture
point, Postgres NULL ordering, and the maintenance-mode admin lockout gap
listed above were all fixes applied after that review, not part of the
original draft.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 08:45:44 +02:00
admin 53789ca56e fix(security): close remaining CSP report-only gaps, make Sentry sourcemap upload verbose for debugging
- connect-src: allow *.fastly.mux.com (Mux HLS chunks/manifests via Fastly
  edge, dynamic regional subdomains) and *.litix.io (Mux Data beacon uses
  inferred.litix.io, not covered by the exact litix.io entry)
- media-src: allow image.mux.com (storyboard.vtt thumbnail track)
- next.config.ts: silent:true -> silent:false + debug:true so the next
  Docker build surfaces what the Sentry/GlitchTip sourcemap upload plugin
  actually does (still minified stacktraces in GlitchTip despite the
  build-arg fix in b469389)
2026-08-14 07:11:30 +02:00
admin b469389654 fix(security): close CSP report-only gaps, wire Sentry sourcemap upload into Docker build
- connect-src: allow the GlitchTip domain itself so the SDK can report events
- media-src: allow Mux's edge CDN (*.edgemv.mux.com) for HLS manifests
- img-src: allow blob: for client-side upload previews
- pass SENTRY_ORG/SENTRY_PROJECT/SENTRY_AUTH_TOKEN as Docker build args so
  withSentryConfig can upload readable source maps during `npm run build`
2026-08-13 20:13:00 +02:00
admin b4a49f8ed3 fix: address findings from full security/functionality audit (Gitea #17)
Rate-limiting on messages.send, reports.create, reactions.toggle,
follows.create, reposts.create. Unique constraints on Report to stop
report-flooding. Mux webhook now fails hard in production without a
signing secret instead of skipping verification. Admin-panel outline
buttons fixed on 4 more pages (same white-on-dark bug as the Users page).
Timing-safe comparison for ADMIN_SECRET/CRON_SECRET. Fixed duplicate HSTS
header (Cloudflare's stronger one was being shadowed by the app's own).
tRPC now returns clean JSON 401s instead of HTML redirects when
unauthenticated, and no longer blocks public procedures for logged-out
visitors. 14 previously-silent catch blocks now log to GlitchTip.
invite/page.tsx and EmergencyVetSection.tsx fully localized (were 100%
hardcoded German). Stale TODO comments removed, auth.test.ts scaffold
rewritten to match the actual lazy-upsert pattern instead of a Clerk
webhook flow that was never built.
2026-08-13 12:42:49 +02:00
admin 13513583bc feat(security): ship CSP in Report-Only mode via Clerk's middleware option
Replaces the dead hand-rolled buildCsp() draft in next.config.ts (never
wired in — its script-src had no 'unsafe-inline'/nonce, which blocked
Next.js's own hydration script and blanked the whole app when tested
live on 2026-08-10) with Clerk's official contentSecurityPolicy option
on clerkMiddleware(). This is Clerk's documented workaround: its default
directives already include the permissive script-src Next.js App Router
needs, merged with our app-specific img-src/connect-src/media-src/
font-src/frame-ancestors/object-src/base-uri additions (Supabase, Mux,
litix).

Ships as Content-Security-Policy-Report-Only (reportOnly: true) with
both reporting mechanisms wired to a new /api/csp-report route handler:
Clerk's reportTo (Reporting API) and a classic report-uri (for browsers
without Reporting API support). The route normalizes both report shapes,
rate-limits by IP, and forwards violations to Sentry/GlitchTip as
warnings — never blocks the request.

Deliberately NOT enforced yet — move to reportOnly: false only after a
clean observation period with zero unexpected violations.

Gitea #5
2026-08-12 13:42:08 +02:00
admin 07a1cb7768 feat(landing): add a public landing page at / (Gitea #4 follow-up)
/ unconditionally redirected to /feed, gated entirely by proxy.ts's
auth.protect() — so logged-out visitors AND social-media crawlers
scraping og:image for link previews got bounced to Clerk's hosted
sign-in before Next.js ever rendered this route's metadata (surfaced
as Facebook Sharing Debugger's "og:image should be explicit" warning,
since the crawler never saw our actual tags).

Adds "/" to proxy.ts's public matcher and moves the
authenticated-redirect into the page itself via a plain auth() read
(not .protect()), so it can render a real hero page — matching the OG
image's warm gradient + paw-mark visual language — for logged-out
visitors and crawlers alike, while still bouncing authenticated users
straight to /feed as before.
2026-08-11 19:25:52 +02:00
admin 747d9e1811 fix(proxy): allow unauthenticated access to /opengraph-image
Social-media crawlers (Facebook, Twitter/X, Slack, Discord) never
have a Clerk session, so the new OG image route was 307-redirecting
every one of them to /sign-in instead of serving the image — found
while live-testing right after deploy, same class of bug as the
GDPR-export route fix.
2026-08-11 15:29:28 +02:00
admin 83513828c6 fix(proxy): allow unauthenticated access to /api/gdpr-export/[token]
The Clerk route matcher was redirecting the new GDPR export download
link to /sign-in, breaking the pawfeed.org token flow just shipped —
found while live-testing the link right after deploy.
2026-08-11 14:27:10 +02:00
admin c744bee9dc fix(cron): exempt /api/cron/* from Clerk auth.protect()
trim-feeds and anniversaries validate their own CRON_SECRET bearer
token, but Clerk's middleware ran auth.protect() first and 307-redirected
every request (including the cron caller) to sign-in before the route
handler's own check ever ran. Same pattern as /api/webhooks/(.*), which
also authenticates itself independently of Clerk.
2026-07-21 21:09:22 +02:00
admin e5264e3ca3 feat(legal): add Nutzungsbedingungen page and link in sidebar footer 2026-06-21 20:58:10 +02:00
admin 5800e06cd9 feat: ad engagement, invite gate, sidebar footer, docker setup
Ad interactions (reactions/comments/reposts on ads):
- Schema: AdReaction, AdComment, AdRepost models with RLS enabled
- tRPC: new ads router (getFeedData, toggleReaction, addComment, listComments, toggleRepost)
- AdCard: fully interactive card with paw, comment sheet, repost — mirrors PostCard style
- getActiveAds includes _count for advertiser engagement metrics

Invite system enforcement:
- proxy.ts: /sign-up blocked without pf_invite cookie
- INVITE_REQUIRED=false env var disables gate for post-beta live launch
- Full invite flow: /join → cookie → /sign-up → consume-invite → 3 codes issued to new user
- Admin: listInviteCodes, createRootInvite, revokeInvite procedures

Sidebar mini-footer:
- Impressum + Datenschutz links always visible at sidebar bottom
- PawFeed Alpha-Test copyright line

Docker self-hosting:
- docker/Dockerfile: 3-stage build using Next.js standalone output
- docker/docker-compose.yml: env_file + build-args for NEXT_PUBLIC_ vars
- .dockerignore at project root
- next.config.ts: output standalone for minimal image
- .env.example: documented INVITE_REQUIRED
2026-06-21 18:12:13 +02:00
admin ee658b4788 fix(middleware): migrate to proxy.ts for Next.js 16, enable Supabase RLS on all tables 2026-06-21 15:56:32 +02:00
admin 5f781fc20e fix(01-02): rename src/middleware.ts to src/proxy.ts — Next.js 16 convention 2026-06-06 10:01:29 +02:00