Commit Graph

6 Commits

Author SHA1 Message Date
Arnaud f6975e98a9 fix: recipe/storage tier limits are lifetime totals, not monthly (v0.58.0)
Recipe count and storage usage shared the monthly user_usage bucket with
AI calls, so both incorrectly reset every month even though nothing was
deleted. Only AI calls should be monthly.

Recipe count and storage are now derived live from real data (recipes,
recipe/review photos, avatar) instead of a counter — deleting a photo or
recipe is itself the "decrement", no extra wiring needed. Storage size is
tracked per-row (recipePhotos.sizeMb, ratings.photoSizeMb, users.avatarSizeMb)
and threaded through presign -> upload -> save.

Also fixes avatar removal silently no-oping (client sent a field the PATCH
schema didn't recognize).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 20:32:31 +02:00
Arnaud 3042d289a0 security: fix full audit findings (v0.32.0)
Full list of the audit's confirmed findings and their fixes:

- Stored XSS via unescaped JSON-LD on the public recipe page
  (app/r/[id]/page.tsx) — escape < before injecting.
- CSP allowed unsafe-eval in production — now dev-only (Next prod
  never eval()s; only its HMR does).
- avatarUrl accepted any URL with no ownership check — now takes an
  avatarKey issued by avatar-presign, validated server-side, same
  pattern as recipe/review photos.
- No session revocation on password change/reset — both now revoke
  other sessions (revokeOtherSessions: true, revokeSessionsOnPasswordReset).
- Rate-limit bypass via spoofable X-Forwarded-For — take the last
  (proxy-appended) hop instead of the first (client-supplied) one,
  matching the single-Traefik-hop topology.
- Webhook signing secrets stored plaintext — now AES-256-GCM
  encrypted like every other secret in this app, with a legacy-
  plaintext fallback for pre-existing rows (bare hex has no ":", our
  ciphertext format always does).
- Better Auth's own rate limiter defaulted to in-memory storage,
  ineffective across replicas — now backed by the same Redis as
  lib/rate-limit.ts (secondaryStorage), with storeSessionInDatabase
  explicit so session storage itself doesn't move as a side effect.
- Presigned upload URLs didn't bind the declared file size to the
  actual upload, letting a client under-declare size (and quota
  charge) then PUT an arbitrarily large object — switched to S3
  presigned POST with a signed content-length-range condition,
  enforced by the storage server itself.
- generateMetadata() on the recipe page skipped the visibility
  filter the page body uses, leaking a private recipe's title via
  <title> to any signed-in user with the id.
- Block/unblock had no rate limit, unlike follow/unfollow.
- AI quota was charged even when a user's own BYOK key was used
  (their own credentials/billing) — added an isByok flag through
  the config-resolution chain and skip the charge when set. Also
  wired BYOK into generate/generate-from-idea/translate/import-url,
  which never looked it up at all before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 15:05:05 +02:00
Arnaud 546ba98d2f fix: photos lost on save, multi-upload only kept last file, thumbnails 400
recipe-form/API routes were never sending/persisting photos on create+update.
PhotoUploader accumulated uploads via a stale closure over the photos prop, so
only the last file of a multi-select survived. Once both were fixed, thumbnails
still 400'd because Next's image optimizer blocks upstream hosts resolving to
private/loopback IPs (localhost:9000 MinIO) — skip optimization for
storage-hosted photos since they're already web-sized user uploads.
2026-07-12 13:16:37 +02:00
Arnaud 362f65656b fix: audit fixes — tier-quota bypass, webhook SSRF, auth hardening, pagination, a11y
Full audit (bugs/UI-UX/backend/feature-gap) turned up a money-leak AI quota
bypass, webhook SSRF, and a long tail of missing pagination/auth/a11y work.
Fixes land together since HANDOFF.md tracked them as one backlog.

- AI routes charge tier quota before generating; nutrition POST is author-only
- Webhook dispatch re-validates URL per delivery (SSRF/DNS-rebinding), treats
  redirects as failures; recipe.published now actually dispatches
- New indexes/unique constraints on recipes, meal-planning, comments FK cascade
- Recipe PUT/restore snapshot only inside the transaction, after validation
- Recipe DELETE cleans up S3 objects (recipe + review photos)
- Optimistic UI (favorite/star/follow/shopping-list) rolls back on failure
- Upload presign enforces file size cap + per-tier storage quota
- Route-level loading/error/not-found states across (app), admin, and root
- middleware.ts guards (app)/admin; requireAdmin checks DB role, not cached
  session; rate limiting applied to both session and API-key branches,
  bucketed per key; Stripe webhook dedupes by event id
- Pagination added to recipes, feed, profile, comments, pantry, admin tables
- Nav shows real avatar + profile link + dark-mode toggle; destructive actions
  standardized on AlertDialog
- Unsaved-changes guard + real ingredient/step validation on recipe form;
  canonical /recipes/[id] used in-app; next/image migration; aria-labels and
  alt text across icon buttons, avatars, recipe photos
- packages/api-types removed (zero callers, too drifted to safely rewire);
  openapi.ts and ai-keys error shape drift fixed; BYOK decrypt failures now
  surface instead of silently falling back to the platform key

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 21:50:35 +02:00
Arnaud 01fdbb880b feat(i18n): translate hardcoded strings across print, public recipe, and recipe management UI
Wires dozens of components and server pages to next-intl instead of hardcoded
English text, and adds a lightweight getMessages/formatMessage helper for
server components (print pages, public recipe page, cook metadata) since
next-intl/server isn't wired into this app's routing. Backfills missing
en/fr message keys that existing code already referenced but fr.json lacked.
2026-07-02 07:58:18 +02:00
Arnaud 84d6cfeb07 feat(recipes): full recipe CRUD with photos, print, version history
List, detail, new, edit pages. Server-side pagination, dietary tags, difficulty.
Photo upload to S3-compatible storage. Version history. Multi-select grid with
bulk delete/visibility. Print view. Delete confirmation dialog.
2026-07-01 08:10:11 +02:00