Fix #423: org membership rail #429

Merged
crueber merged 2 commits from fix/issue-423 into main 2026-09-13 00:09:36 +00:00
Owner

Fixes #423 — owner profile Organizations membership list (backend rail was missing).

DECISION (endpoint vs profile field): dedicated endpoint GET /api/v1/users/{principal}/orgs over Service.MemberOrgsFor — NOT a member_orgs profile field. Rationale: core internal/api must never import identity (law 8 — a field needs a new seam); the endpoint keeps the LIST-plus-probes cost off every profile GET (law 6) and leaves profileETag untouched (no #382 entanglement). Visibility: no filtering — every containing roster served to read-authorized callers (anonymous needs anonymous_read, the profile/members gate); org visibility governs repos, not roster facts. Org profiles omit the section (member principals are email spellings, not routable owner slugs per #370 — roster lives at organization settings). Full rationale in docs/features/01_identity_permissions.md §Decisions.

What lands:

  • Backend: GET /api/v1/users/{principal}/orgs (both lanes via handleTop; no /services/api twin — like every identity surface), sorted names, []-never-null, 200 for unknown principals, GET-only; mutable-collab + content ETag; ExposedTemplates extended (composition already registers the slice wholesale).
  • SDK: client.users.orgs(principal).
  • Client: user-profile Organizations section with org links + explicit No organizations empty state (never absent).
  • Docs: features/01 §8 table + Decisions entry; Apidocs table.

Tests: identity -race green, routeUserOrgs 100%, package 95.7% (≥95 gate); node 891 pass / 0 fail (2 server-smoke tests skip on cold port — :8080 here answers from a foreign live instance, untouched); vite build green; gofmt/vet clean; no new deps. Browser: not driven (shared daemon blocks loopback per task note) — reasoning + headless coverage only.

Fixes #423 — owner profile Organizations membership list (backend rail was missing). DECISION (endpoint vs profile field): dedicated endpoint GET /api/v1/users/{principal}/orgs over Service.MemberOrgsFor — NOT a member_orgs profile field. Rationale: core internal/api must never import identity (law 8 — a field needs a new seam); the endpoint keeps the LIST-plus-probes cost off every profile GET (law 6) and leaves profileETag untouched (no #382 entanglement). Visibility: no filtering — every containing roster served to read-authorized callers (anonymous needs anonymous_read, the profile/members gate); org visibility governs repos, not roster facts. Org profiles omit the section (member principals are email spellings, not routable owner slugs per #370 — roster lives at organization settings). Full rationale in docs/features/01_identity_permissions.md §Decisions. What lands: - Backend: GET /api/v1/users/{principal}/orgs (both lanes via handleTop; no /services/api twin — like every identity surface), sorted names, []-never-null, 200 for unknown principals, GET-only; mutable-collab + content ETag; ExposedTemplates extended (composition already registers the slice wholesale). - SDK: client.users.orgs(principal). - Client: user-profile Organizations section with org links + explicit No organizations empty state (never absent). - Docs: features/01 §8 table + Decisions entry; Apidocs table. Tests: identity -race green, routeUserOrgs 100%, package 95.7% (≥95 gate); node 891 pass / 0 fail (2 server-smoke tests skip on cold port — :8080 here answers from a foreign live instance, untouched); vite build green; gofmt/vet clean; no new deps. Browser: not driven (shared daemon blocks loopback per task note) — reasoning + headless coverage only.
Backend (internal/identity, Forgejo #423): new GET /api/v1/users/{principal}/orgs
over the existing Service.MemberOrgsFor (sorted names, []-never-null, 200 for
unknown principals, GET-only, both lanes via handleTop). DECISION (recorded in
docs/features/01 §Decisions): dedicated endpoint, not a member_orgs profile
field — core internal/api must never import identity (law 8), and the endpoint
keeps the LIST-plus-probes cost off every profile GET (law 6) with profileETag
untouched (no #382 entanglement). Visibility: no filtering — every containing
roster served to read-authorized callers (anonymous needs anonymous_read, the
profile/members gate). Cache: mutable-collab + content ETag (busts on roster
change, 304s when still); bio edits ride a separate route so the rail stays
fresh (TestUserOrgsFreshAfterBioEdit). Discovery: ExposedTemplates extended
(the #272 opt-in; composition already registers the slice wholesale, both
pinned tests extended).
SDK (web/sdk/src/users.js): client.users.orgs(principal).
Client (web/src/pages/Repos.jsx): user-profile Organizations section (org
links to /:org; explicit 'No organizations' empty state, never absent); org
profiles omit it (member principals are email spellings, not routable slugs
per #370 — roster lives at organization settings).
Docs: features/01 §8 table + Decisions; Apidocs table.
Sign in to join this conversation.
No description provided.