feat(profile): consistent profile menu, first name, uploadable avatar (BUNYIP-408) #397
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/BUNYIP-408-profile-menu-avatar"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Implements BUNYIP-408: a consistent profile menu on every shell, the user shown by first name instead of a raw email, and an uploadable avatar replacing the generic silhouette.
Changes
bunyip-web/src/views/layout.rs): a newprofile_menu(avatar button opening a<details>dropdown with Profile + Log out) replaces the standalone logout link and the raw-email text inapp_topbar(dashboard + admin shells) and in the publicheader, so the documentation and marketing pages reach profile info through the identical affordance. Dismissal is click-away / Escape via one delegated listener; keyboard-accessible, no framework.User::display_nameprefers the first name and falls back to the email local part (never the full email).avatar_badgerenders the uploaded image, or a gradient circle with the initial when unset.user_avatarsBYTEA table +users.avatar_updated_at): mirrors the feedback-attachment hardening (BUNYIP-90). Bytes never touch a filesystem or static mount; they are served only through an authenticated bunyip-api handler with an explicit imageContent-TypeandContent-Disposition: inline. bunyip-api has no static file mount, so an avatar can never be served from an origin where it could execute (the security constraint the ticket flags).avatar_updated_atis the cheap existence marker (keeps the BYTEA off the hot user-fetch path) and the?v=cache-buster, set/cleared in the same transaction as the blob.POST/DELETE/GET /v1/users/me/avatar; webPOST /settings/avatar,POST /settings/avatar/remove, same-origin proxyGET /me/avatar. The Settings Profile card gains an avatar preview, upload form, and remove button.Note on the ticket's proposed
avatar_urlcolumnThe ticket suggested a nullable
avatar_urlstring column. Storing a raw URL a user controls, or serving uploaded bytes from a static path, is exactly the "executable origin" risk the ticket also flags. Instead the bytes are stored server-side and served through a controlled handler (matching the existing feedback-attachment precedent), and the client derives the same-origin<img>src fromavatar_updated_at. Functionally equivalent, and it satisfies the validation + safe-serving acceptance criteria by construction.Acceptance criteria
Verification
just check-containergreen: fmt + clippy-D warnings+ all workspace test binaries. New unit tests: content-sniff / dimension guards (APIhandlers::avatar), anddisplay_name/avatar_initial/avatar_src(webapi::types).octet_length/size_bytesmismatch and an over-2-MiB blob.users.avatar_updated_atpresent astimestamptz.🤖 Generated with Claude Code