feat: add institutional membership tiers and correct member directory
Corrections per 2026-04 institutional review:
- MLFO reclassified as Global Family Office (was incorrectly labeled central bank)
- BIS Innovation Hub reclassified as Standards Body (does not hold observer seat)
- Added missing entities: ICCC, SAID, PANDA, Order of Hospitallers (XOM)
- Added BRICS founding + expanded member central banks (10 entries)
New institutional tier taxonomy (7 tiers):
sovereign_central_bank, global_family_office, settlement_member,
infrastructure_operator, oversight_judicial, delegated_authority,
standards_body
Backend changes:
- New auth/membership.go: tier types, DefaultTrackForTier mapping,
MembershipStore with DB queries for member directory
- New migration 0017: institutional_members + institutional_member_wallets
tables with seed data for all corrected members
- Updated wallet_auth.go getUserTrack(): now resolves institutional
membership (via wallet junction table) before defaulting to Track 1
- WalletAuthResponse now includes institutional_tier and institution_name
- New REST endpoints: GET /api/v1/membership/{tiers,members,members/:slug}
- Added TrackLabel() helper in featureflags
Frontend changes:
- Added InstitutionalTier type and label map to access.ts
- WalletAccessSession extended with institutionalTier/institutionName
- Navbar getAccessTier() now displays institutional tier label when present
- Session summary shows institution name
Co-Authored-By: Nakamoto, S <defi@defi-oracle.io>
This commit is contained in:
89
backend/api/rest/membership.go
Normal file
89
backend/api/rest/membership.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/explorer/backend/auth"
|
||||
)
|
||||
|
||||
// handleMembershipTiers returns the canonical set of institutional tiers
|
||||
// with their labels and default explorer access tracks.
|
||||
// GET /api/v1/membership/tiers
|
||||
func (s *Server) handleMembershipTiers(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeMethodNotAllowed(w)
|
||||
return
|
||||
}
|
||||
|
||||
tiers := auth.ListTiers()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"tiers": tiers,
|
||||
})
|
||||
}
|
||||
|
||||
// handleMembershipMembers returns all active institutional members.
|
||||
// GET /api/v1/membership/members
|
||||
func (s *Server) handleMembershipMembers(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeMethodNotAllowed(w)
|
||||
return
|
||||
}
|
||||
|
||||
if !s.requireDB(w) {
|
||||
return
|
||||
}
|
||||
|
||||
store := auth.NewMembershipStore(s.db)
|
||||
members, err := store.ListMembers(r.Context())
|
||||
if err != nil {
|
||||
writeInternalError(w, err.Error())
|
||||
return
|
||||
}
|
||||
if members == nil {
|
||||
members = []auth.InstitutionalMember{}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"members": members,
|
||||
"total": len(members),
|
||||
})
|
||||
}
|
||||
|
||||
// handleMembershipMemberDetail returns a single member by slug.
|
||||
// GET /api/v1/membership/members/{slug}
|
||||
func (s *Server) handleMembershipMemberDetail(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeMethodNotAllowed(w)
|
||||
return
|
||||
}
|
||||
|
||||
if !s.requireDB(w) {
|
||||
return
|
||||
}
|
||||
|
||||
slug := strings.TrimPrefix(r.URL.Path, "/api/v1/membership/members/")
|
||||
if slug == "" {
|
||||
writeError(w, http.StatusBadRequest, "MISSING_SLUG", "Member slug is required")
|
||||
return
|
||||
}
|
||||
|
||||
store := auth.NewMembershipStore(s.db)
|
||||
member, err := store.GetMemberBySlug(r.Context(), slug)
|
||||
if err != nil {
|
||||
writeInternalError(w, err.Error())
|
||||
return
|
||||
}
|
||||
if member == nil {
|
||||
writeNotFound(w, "Member")
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"member": member,
|
||||
})
|
||||
}
|
||||
@@ -67,6 +67,11 @@ func (s *Server) SetupRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/v1/access/usage", s.handleAccessUsage)
|
||||
mux.HandleFunc("/api/v1/access/audit", s.handleAccessAudit)
|
||||
|
||||
// Institutional membership directory (public, read-only)
|
||||
mux.HandleFunc("/api/v1/membership/tiers", s.handleMembershipTiers)
|
||||
mux.HandleFunc("/api/v1/membership/members", s.handleMembershipMembers)
|
||||
mux.HandleFunc("/api/v1/membership/members/", s.handleMembershipMemberDetail)
|
||||
|
||||
// Track 1 routes (public, optional auth)
|
||||
// Note: Track 1 endpoints should be registered with OptionalAuth middleware
|
||||
// mux.HandleFunc("/api/v1/track1/blocks/latest", s.track1Server.handleLatestBlocks)
|
||||
|
||||
Reference in New Issue
Block a user