Skip to content

fix(users): store avatars as bytea in DB + login hydration & badge-403 polish#1268

Merged
ToddHebebrand merged 2 commits into
mainfrom
fix/avatar-db-storage-and-ui-polish
Jun 12, 2026
Merged

fix(users): store avatars as bytea in DB + login hydration & badge-403 polish#1268
ToddHebebrand merged 2 commits into
mainfrom
fix/avatar-db-storage-and-ui-polish

Conversation

@ToddHebebrand

Copy link
Copy Markdown
Collaborator

What

Three fixes that came out of the v0.70 release-checklist testing pass, hardened by a multi-agent review round.

1. Avatar storage → DB bytea (the headline fix). POST /users/me/avatar 500'd with EACCES: permission denied, open '/data/avatars/….png.tmp' — the API runs as uid=1001(hono) but the api_data volume's /data/avatars is root:root 0755, so filesystem avatar storage (#1059) was broken in any deployment with that ownership, prod included. Avatars now live on the user's own row as avatar_data bytea + avatar_mime + avatar_updated_at (migration 2026-06-11-j): no volume dependency, works across replicas, and the existing dual-axis users RLS is the access boundary. statAvatar sizes via octet_length() so the conditional-GET 304 path never transfers the blob. The migration clears only dangling internal /api/v1/users/%/avatar URLs (external pre-upload-era URLs still resolve) and always logs the count.

2. React #418 hydration error on /login. cfAccessRedirectChecked was seeded from a typeof window helper — true on the server (renders the form), false on a clean client load (renders the placeholder) — so every login visit hydrated mismatched trees. Now a constant false with the skip decision inside the effect; a regression test asserts renderToString output is identical with and without window. The CF-config fetch that gates the form also gets a 4s AbortSignal.timeout so a hung request can't pin login behind an empty placeholder.

3. Per-page 403 console error. The sidebar fired GET /admin/account-deletion-requests/pending-count for every user, but the endpoint requires platform admin — so every page load logged a 403 for everyone (no platform admin exists in prod). /users/me and the login/MFA payloads now expose isPlatformAdmin, the store merges it on the preferences refresh (heals persisted pre-field sessions), and the sidebar hides the nav item + skips the badge fetch without it.

Review round (multi-agent, both commits)

The second commit addresses the findings: the critical one was that isPlatformAdmin initially only rode the /users/me response, which never reaches the store on password login — the gate would have hidden the nav from the exact users it serves. Also fixed there: schema/migration timestamptz drift, Sentry capture on the avatar write catch + soft-500 branches, invalid-avatar_mime detection (corruption no longer masquerades as "no avatar"), and stale filesystem-era comments.

Testing

  • users.test.ts 53/53 — avatar I/O mocked behind an in-memory store with the real sniffing/ETag helpers kept live; new 304 conditional-GET and 500-path tests; vestigial db.update mocks removed
  • New integration test avatar-bytea-roundtrip 3/3 against the real driver as breeze_app: byte-exact round-trip (all 256 byte values), octet_length sizing, RLS fail-closed across partners (the fix(api): partner-scoped custom fields fail RLS — 500 on "add custom field" #1257 class of gap mocked suites can't see)
  • login.test.ts 4/4 incl. new payload regression tests; LoginPage.test.tsx 8/8 incl. the hydration-invariant test
  • pnpm db:check-drift clean (237 files); web + api tsc clean
  • Not yet done: live browser verify of the upload (needs the API image rebuilt from this branch — the running local container predates it)

🤖 Generated with Claude Code

ToddHebebrand and others added 2 commits June 11, 2026 20:21
…3 polish

- Avatars move from /data/avatars filesystem (broken by root-owned volume,
  EACCES -> 500 on upload) to a bytea blob on the users row (avatar_data,
  avatar_mime, avatar_updated_at). Works across replicas, no volume perms.
  Migration clears dangling filesystem avatar_urls with a logged count.
- Fix React #418 on /login: cfAccessRedirectChecked seeded from a
  typeof-window helper diverged between SSR and first client render; now a
  constant false with the skip decision in the effect. Regression test
  renders with and without `window` and asserts identical HTML.
- Stop the per-page 403 console error: /users/me now exposes isPlatformAdmin
  and the sidebar gates the Deletion-requests nav item + pending-count badge
  fetch on it (endpoint requires platform admin, not users:write).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… observability

Addresses multi-agent PR review of 03e35e1:

- CRITICAL: isPlatformAdmin never reached the auth store on password login
  (login/MFA payloads omitted it; fetchAndApplyPreferences only merged
  preferences) — platform admins lost the deletion-requests nav the flag
  gates. Now in both payloads + merged on the /users/me refresh, with
  login-payload regression tests.
- statAvatar computes size via octet_length instead of pulling the blob —
  the 304 fast path no longer transfers up to 5 MB to send no body; doc now
  matches behavior. Invalid avatar_mime with bytes present is logged +
  captured instead of masquerading as "no avatar".
- Schema: avatar_updated_at gets withTimezone to match the migration's
  timestamptz (drift), and the migration's dangling-URL cleanup now only
  clears internal /api/v1/users/%/avatar URLs (external pre-upload-era URLs
  still resolve) and always logs the count.
- Observability: POST avatar catch + the three soft-500 branches now
  captureException/log (previously console-only or silent); login CF-config
  fetch gets a 4s timeout so a hung request can't pin the login form behind
  the empty placeholder, and its catch warns instead of swallowing.
- Tests: bytea round-trip integration test (real driver + RLS fail-closed,
  octet_length sizing, byte-exact storage), 304 conditional-GET and
  500-path route tests, vestigial db.update mocks removed; stale
  filesystem-era comments rewritten.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying breeze with  Cloudflare Pages  Cloudflare Pages

Latest commit: ac49be1
Status: ✅  Deploy successful!
Preview URL: https://61e6de85.breeze-9te.pages.dev
Branch Preview URL: https://fix-avatar-db-storage-and-ui.breeze-9te.pages.dev

View logs

@ToddHebebrand ToddHebebrand merged commit 6b5b908 into main Jun 12, 2026
33 checks passed
@ToddHebebrand ToddHebebrand deleted the fix/avatar-db-storage-and-ui-polish branch June 12, 2026 07:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant