Repository navigation
Add user profiles, avatar upload, and publisher image uploads - #10
Conversation
Users can now customize their account at /dashboard/settings and upload avatars/banners in the publisher editor. Images are optimized in the browser (downscale + WebP) and validated server-side against their real bytes. - migrations/0004_user_profiles.sql: users.profile_customized flag so the login upsert stops overwriting a saved custom name/avatar. - src/lib/image.ts: shared image validation (magic bytes vs declared type, size, header-parsed dimensions). Avatars cap at 512px, banners at 1600px, 1.5MB per image (D1 rows cap at 2MB). - src/lib/image-client.ts: browser-only canvas optimizer; the preview is exactly the bytes submitted. - src/lib/media.ts: accepts an uploaded data URL or a plain http(s) URL. - src/pages/api/user/profile.ts: GET/PATCH the signed-in user's profile. - src/pages/api/publishers/[id].ts: avatar/banner now accept image uploads. - src/components/ImageField.astro + settings page + navbar/dashboard avatar. The custom avatar lives in D1 and is overlaid on the session at render time, since a base64 image cannot fit the ~4KB signed session cookie. Tests: 38 passing (15 new).
Deploying openrom-store with
|
| Latest commit: |
4cedad8
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://9c671ddf.openrom-store.pages.dev |
| Branch Preview URL: | https://feat-user-profiles-image-upl.openrom-store.pages.dev |
|
Caution Review failedThe pull request is closed. Walkthrough
Merge Risk:
|
Addresses three findings on #10. 1. Major — session cookie could exceed the browser limit. The auth callback built the cookie from the row `upsertUser` returns, and after a user saved an uploaded avatar that row holds the multi-megabyte data URL. A real upload produced a 1,066,871-byte cookie, which browsers reject — the user would fail to sign in again. The cookie is now built from the GitHub profile via `sessionUserFromGitHubProfile`, so it stays ~230 bytes and the GitHub name/avatar fallback survives a cleared custom avatar. 2. Minor — the image cap was checked against decoded bytes only. What D1 stores is the base64 text (~4/3 the decoded size), so a 1.5 MB image was ~2 MB of text and a row with an avatar plus a banner could exceed D1's 2 MB row limit, failing at the database with a generic 500. Lowered the decoded cap to 600 KB and added a hard 800 KB bound on the encoded string itself — which also catches whitespace-padded values that the decoded-size check alone missed. The client optimizer now targets the same encoded budget. 3. Minor — the upload label had no keyboard focus indicator (the real file input is sr-only). Added focus-within styles. Tests: 41 passing (3 new).
What changed and why
Users can now customize their own account and upload real images for avatars and publisher branding — no external image host, no upload endpoint.
Three user-facing additions:
/dashboard/settings) — set a custom display name and avatar.The key design decision
The session is a stateless HMAC-signed cookie capped at ~4 KB. A base64 avatar blows past that, so the custom image lives in D1 and is overlaid on the session at render time:
Because GitHub supplies
name/avatar_urland they are refreshed on every login, a saved custom profile sets aprofile_customizedflag that tells the login upsert to leave the user's own values alone. Without it, a custom avatar would be wiped by the next sign-in.How uploads work
No
/api/uploadendpoint — the browser does the work:The preview is the submitted value. The server then re-validates from scratch (
src/lib/image.ts): declared MIME vs real magic bytes, decoded size, and dimensions parsed from the file header.1.5 MB/image keeps a row holding both an avatar and a banner under D1's 2 MB row limit. A plain
http(s)URL is still accepted in place of an upload.Files
migrations/0004_user_profiles.sqlsrc/lib/image.ts,src/lib/media.tssrc/lib/image-client.ts,src/components/ImageField.astrosrc/pages/api/user/profile.ts(newGET/PATCH),src/pages/api/publishers/[id].tssrc/pages/dashboard/settings.astro,src/pages/dashboard.astro,src/components/Navbar.astro,src/layouts/Layout.astro,src/pages/dashboard/publisher/[id].astroREADME.md,tests/user-profile.test.tsVerification
npm run build✅ ·npx tsc --noEmit✅ clean ·npm test✅ 38/38 (15 new)wrangler pages devwith a real local D1 and a genuinely signed session cookie:PATCH /api/user/profileandPATCH /api/publishers/[id]→401200, then rendered on the settings page, navbar, and publisher page400), a PNG labelled as WebP (400), andjavascript:URLs (400)Reviewer notes
0004must be applied to the remote D1 (npx wrangler d1 migrations apply openrom-store --remote) before or with the Pages deploy, or profile saves fail against the missing column.src/lib/media.tsimports its siblings with an explicit.tsextension because Node's--experimental-strip-typesloader (used bynpm test) does not resolve extensionless relative imports. The Astro/Vite build accepts it.Navbar.astronow types its prop as a localNavbarUserinterface rather thanSessionUser, since the value it receives is the D1-overlaid profile.Summary by CodeRabbit