Org profile parity + avatar: location/timezone/bio fields and avatar storage #359

Closed
opened 2026-09-12 00:48:51 +00:00 by crueber · 3 comments
Owner

Survey: crueber/walhub#349 candidates 2 + 8 (bundled — shared implementation surface).

Evidence

  • Org shape is display_name + description only (internal/identity/orgs.go:26-33; PUT via PutOrg edits just those two). User-owner profiles carry display_name, location, timezone, bio_markdown (internal/api/profile.go:51; validated, max lengths; PUT /owners/{owner}/profile).
  • No avatar field or storage anywhere: grep avatar|Avatar|logo across internal/ + web/src finds nothing on identity surfaces; Org.jsx ProfileTab edits display_name/description only.
  • GitHub/Forgejo orgs have avatars + location/website. Repo-attachment/LFS blob storage is the precedent for where avatar bytes could live (bucket-backed, not local disk — law 4).

Design

  • Extend org.json (append-only fields) with location/timezone/bio (or description stays + website?) matching the owner-profile spelling/validation; extend PUT /api/v1/orgs/{org} + Org.jsx ProfileTab.
  • Avatar: add avatar key + upload/get endpoints (size-capped, content-typed, bucket object under orgs//), render on the /:owner org profile page and org settings header.

Acceptance criteria

  • Org profile round-trips the new fields via API + UI; validation mirrors owner-profile limits.
  • Avatar upload/display works; bytes live on the bucket (wipe-safe per law 4).
  • Bucket shape stays back-compatible (append-only; old readers ignore new fields).
Survey: crueber/walhub#349 candidates 2 + 8 (bundled — shared implementation surface). ## Evidence - Org shape is display_name + description only (internal/identity/orgs.go:26-33; PUT via PutOrg edits just those two). User-owner profiles carry display_name, location, timezone, bio_markdown (internal/api/profile.go:51; validated, max lengths; PUT /owners/{owner}/profile). - No avatar field or storage anywhere: grep avatar|Avatar|logo across internal/ + web/src finds nothing on identity surfaces; Org.jsx ProfileTab edits display_name/description only. - GitHub/Forgejo orgs have avatars + location/website. Repo-attachment/LFS blob storage is the precedent for where avatar bytes could live (bucket-backed, not local disk — law 4). ## Design - Extend org.json (append-only fields) with location/timezone/bio (or description stays + website?) matching the owner-profile spelling/validation; extend PUT /api/v1/orgs/{org} + Org.jsx ProfileTab. - Avatar: add avatar key + upload/get endpoints (size-capped, content-typed, bucket object under orgs/<org>/), render on the /:owner org profile page and org settings header. ## Acceptance criteria - [ ] Org profile round-trips the new fields via API + UI; validation mirrors owner-profile limits. - [ ] Avatar upload/display works; bytes live on the bucket (wipe-safe per law 4). - [ ] Bucket shape stays back-compatible (append-only; old readers ignore new fields).
crueber added this to the v1 milestone 2026-09-12 00:48:51 +00:00
Author
Owner

Fixed by #366 (#366): org.json append-only location/timezone/bio_markdown (+ avatar pointer) with owner-profile validation; PUT route + avatar GET/PUT/DELETE endpoints (bucket-backed, 2 MiB cap, magic-sniffed); UI edits + renders on /:owner and settings. No website field (documented decision). Tests: identity -race green at 96.3% cover; node + vite clean (3 smoke fails are the live setup-only :8080 instance, same on clean main).

Fixed by #366 (https://git.packden.us/crueber/walhub/pulls/366): org.json append-only location/timezone/bio_markdown (+ avatar pointer) with owner-profile validation; PUT route + avatar GET/PUT/DELETE endpoints (bucket-backed, 2 MiB cap, magic-sniffed); UI edits + renders on /:owner and settings. No website field (documented decision). Tests: identity -race green at 96.3% cover; node + vite clean (3 smoke fails are the live setup-only :8080 instance, same on clean main).
Author
Owner

Review of PR #366 (fix/issue-359, 09e4243) — verified in scratch worktree /tmp/pr366 (removed afterward); main worktree left untouched; no browser drive (tests + reasoning only, per brief); no live instance/docker touched.

PASS — all 7 review axes hold:

  1. Back-compat both ways: parseOrg (orgs.go:167) is plain json.Unmarshal — old readers ignore the new keys; new reader sees zero values for pre-#359 docs. New fields are omitempty so untouched orgs stay byte-identical until first edit. PutOrg signature change (displayName,description → OrgEdit) has exactly one non-test caller (http.go:429); nothing else in internal/ or cmd/ calls it.
  2. Validation mirrors owner-profile exactly: 200-rune display/location, 64-byte IANA-shape timezone (same regex as profile.go tzShape), 64 KiB valid-UTF-8 bio; all four 400 strings are byte-identical to internal/api/profile.go:119-128. Duplicated constants justified in-code (law 8: identity must not import api). Website omission documented in orgs.go:25-35 + docs decision entry.
  3. Avatar upload: handler caps via http.MaxBytesReader(max+1) BEFORE buffering (http.go) plus service-level len check → 413 on both paths; magic-sniff PNG/JPEG/GIF/WebP, client Content-Type ignored → 415; PUT/DELETE gated by CheckOrgOwner, GET public behind the standard anonymousRead gate; Cache-Control public max-age=86400 immutable + ?v=avatar_updated_at cache-bust — sane.
  4. Bucket (law 4): bytes at orgs//avatar (identity.go OrgAvatarKey), wipe-safe; DeleteOrg deletes the avatar object (orgs.go:875); pruned-object-under-set-pointer renders as no-avatar, never an error (GetOrgAvatar). Transfer (#358) interplay checked: avatar is org-level, transfers move repos — no gap, no follow-up needed.
  5. PutOrg is full-document replace over all five fields, avatar pointer preserved (untouched by profile PUTs); CAS-loop lost-update is human-rate, same class as owner-profile PUT — acceptable, documented.
  6. UI: ProfileTab edits all five fields (timezone via Intl.supportedValuesOf picker) + upload/remove, all inside the existing canManage owner branch — read-only fallback leaks nothing; OrgAvatar gates on avatar_content_type, hides on 404; renders on /:owner header (Repos.jsx), org settings header, and settings ProfileTab. SDK url/upload/remove + org-profile.js helpers clean; Repos→Org import is one-directional (no cycle).
  7. Law 8: avatar routes registered via identity RouteProvider + ExposedTemplates/discovery (exposed_test.go pins all 3 verbs + browser lane); docs/features/01 updated (schema table, route table, decision entry). No new deps (package.json untouched).

Verification (scratch worktree): go test ./internal/identity/... -race PASS; coverage 96.3% (>=95 gate); gofmt/vet clean; touched-file node tests 7/7 PASS (org-profile + sdk-identity); full node suite 700/703 with web/node_modules symlinked — the 3 failures are the live-:8080 smoke tests (setup-only 503 from the running instance), file-independent and unrelated to this PR; vite build + esbuild SDK bundle clean; go build ./... clean. No fixes pushed — none needed.

MERGE RECOMMENDATION: ready to merge.

Review of PR #366 (fix/issue-359, 09e4243) — verified in scratch worktree /tmp/pr366 (removed afterward); main worktree left untouched; no browser drive (tests + reasoning only, per brief); no live instance/docker touched. PASS — all 7 review axes hold: 1. Back-compat both ways: parseOrg (orgs.go:167) is plain json.Unmarshal — old readers ignore the new keys; new reader sees zero values for pre-#359 docs. New fields are omitempty so untouched orgs stay byte-identical until first edit. PutOrg signature change (displayName,description → OrgEdit) has exactly one non-test caller (http.go:429); nothing else in internal/ or cmd/ calls it. 2. Validation mirrors owner-profile exactly: 200-rune display/location, 64-byte IANA-shape timezone (same regex as profile.go tzShape), 64 KiB valid-UTF-8 bio; all four 400 strings are byte-identical to internal/api/profile.go:119-128. Duplicated constants justified in-code (law 8: identity must not import api). Website omission documented in orgs.go:25-35 + docs decision entry. 3. Avatar upload: handler caps via http.MaxBytesReader(max+1) BEFORE buffering (http.go) plus service-level len check → 413 on both paths; magic-sniff PNG/JPEG/GIF/WebP, client Content-Type ignored → 415; PUT/DELETE gated by CheckOrgOwner, GET public behind the standard anonymousRead gate; Cache-Control public max-age=86400 immutable + ?v=avatar_updated_at cache-bust — sane. 4. Bucket (law 4): bytes at orgs/<org>/avatar (identity.go OrgAvatarKey), wipe-safe; DeleteOrg deletes the avatar object (orgs.go:875); pruned-object-under-set-pointer renders as no-avatar, never an error (GetOrgAvatar). Transfer (#358) interplay checked: avatar is org-level, transfers move repos — no gap, no follow-up needed. 5. PutOrg is full-document replace over all five fields, avatar pointer preserved (untouched by profile PUTs); CAS-loop lost-update is human-rate, same class as owner-profile PUT — acceptable, documented. 6. UI: ProfileTab edits all five fields (timezone via Intl.supportedValuesOf picker) + upload/remove, all inside the existing canManage owner branch — read-only fallback leaks nothing; OrgAvatar gates on avatar_content_type, hides on 404; renders on /:owner header (Repos.jsx), org settings header, and settings ProfileTab. SDK url/upload/remove + org-profile.js helpers clean; Repos→Org import is one-directional (no cycle). 7. Law 8: avatar routes registered via identity RouteProvider + ExposedTemplates/discovery (exposed_test.go pins all 3 verbs + browser lane); docs/features/01 updated (schema table, route table, decision entry). No new deps (package.json untouched). Verification (scratch worktree): go test ./internal/identity/... -race PASS; coverage 96.3% (>=95 gate); gofmt/vet clean; touched-file node tests 7/7 PASS (org-profile + sdk-identity); full node suite 700/703 with web/node_modules symlinked — the 3 failures are the live-:8080 smoke tests (setup-only 503 from the running instance), file-independent and unrelated to this PR; vite build + esbuild SDK bundle clean; go build ./... clean. No fixes pushed — none needed. MERGE RECOMMENDATION: ready to merge.
Author
Owner

Fixed by PR #366 (review clean; back-compat both ways, avatar caps/gates/bucket, no transfer gap verified), merged. Closing.

Fixed by PR #366 (review clean; back-compat both ways, avatar caps/gates/bucket, no transfer gap verified), merged. Closing.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
crueber/walhub#359
No description provided.