fix(users): store avatars as bytea in DB + login hydration & badge-403 polish#1268
Merged
Merged
Conversation
…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>
Deploying breeze with
|
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/avatar500'd withEACCES: permission denied, open '/data/avatars/….png.tmp'— the API runs asuid=1001(hono)but theapi_datavolume's/data/avatarsisroot: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 asavatar_data bytea+avatar_mime+avatar_updated_at(migration2026-06-11-j): no volume dependency, works across replicas, and the existing dual-axisusersRLS is the access boundary.statAvatarsizes viaoctet_length()so the conditional-GET 304 path never transfers the blob. The migration clears only dangling internal/api/v1/users/%/avatarURLs (external pre-upload-era URLs still resolve) and always logs the count.2. React #418 hydration error on
/login.cfAccessRedirectCheckedwas seeded from atypeof windowhelper —trueon the server (renders the form),falseon a clean client load (renders the placeholder) — so every login visit hydrated mismatched trees. Now a constantfalsewith the skip decision inside the effect; a regression test assertsrenderToStringoutput is identical with and withoutwindow. The CF-config fetch that gates the form also gets a 4sAbortSignal.timeoutso 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-countfor every user, but the endpoint requires platform admin — so every page load logged a 403 for everyone (no platform admin exists in prod)./users/meand the login/MFA payloads now exposeisPlatformAdmin, 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
isPlatformAdmininitially only rode the/users/meresponse, 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/migrationtimestamptzdrift, Sentry capture on the avatar write catch + soft-500 branches, invalid-avatar_mimedetection (corruption no longer masquerades as "no avatar"), and stale filesystem-era comments.Testing
users.test.ts53/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; vestigialdb.updatemocks removedavatar-bytea-roundtrip3/3 against the real driver asbreeze_app: byte-exact round-trip (all 256 byte values),octet_lengthsizing, 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.ts4/4 incl. new payload regression tests;LoginPage.test.tsx8/8 incl. the hydration-invariant testpnpm db:check-driftclean (237 files); web + apitscclean🤖 Generated with Claude Code