Files
explorer-monorepo/backend/api/rest/routes.go

211 lines
7.5 KiB
Go
Raw Normal View History

package rest
import (
"fmt"
"net/http"
"strings"
)
// SetupRoutes sets up all API routes
func (s *Server) SetupRoutes(mux *http.ServeMux) {
// Block routes
mux.HandleFunc("/api/v1/blocks", s.handleListBlocks)
mux.HandleFunc("/api/v1/blocks/", s.handleBlockDetail)
// Transaction routes
mux.HandleFunc("/api/v1/transactions", s.handleListTransactions)
mux.HandleFunc("/api/v1/transactions/", s.handleTransactionDetail)
// Address routes
mux.HandleFunc("/api/v1/addresses", s.handleListAddresses)
mux.HandleFunc("/api/v1/addresses/", s.handleAddressDetail)
// Search route
mux.HandleFunc("/api/v1/search", s.handleSearch)
// Stats route
mux.HandleFunc("/api/v2/stats", s.handleStats)
// Etherscan-compatible API route
mux.HandleFunc("/api", s.handleEtherscanAPI)
// Health check
mux.HandleFunc("/health", s.handleHealth)
// MetaMask / dual-chain config (Chain 138 + Ethereum Mainnet)
mux.HandleFunc("/api/config/networks", s.handleConfigNetworks)
mux.HandleFunc("/api/config/token-list", s.handleConfigTokenList)
mux.HandleFunc("/api/config/capabilities", s.handleConfigCapabilities)
// Feature flags endpoint
mux.HandleFunc("/api/v1/features", s.handleFeatures)
// Explorer AI endpoints
mux.HandleFunc("/api/v1/ai/context", s.handleAIContext)
mux.HandleFunc("/api/v1/ai/chat", s.handleAIChat)
mux.HandleFunc("/api/v1/ai/metrics", s.handleAIMetrics)
// Route decision tree proxy
mux.HandleFunc("/api/v1/routes/tree", s.handleRouteDecisionTree)
mux.HandleFunc("/api/v1/routes/depth", s.handleRouteDepth)
// Auth endpoints
mux.HandleFunc("/api/v1/auth/nonce", s.handleAuthNonce)
mux.HandleFunc("/api/v1/auth/wallet", s.handleAuthWallet)
feat(auth): JWT jti + per-track TTLs (Track 4 <=1h) + revocation + refresh endpoint Closes the 'JWT hygiene' gap identified by the review: - 24h TTL was used for every track, including Track 4 operator sessions carrying operator.write.* permissions. - Tokens had no server-side revocation path; rotating JWT_SECRET was the only way to invalidate a session, which would punt every user. - Tokens carried no jti, so individual revocation was impossible even with a revocations table. Changes: Migration 0016_jwt_revocations (up + down): - CREATE TABLE jwt_revocations (jti PK, address, track, token_expires_at, revoked_at, reason) plus indexes on address and token_expires_at. Append-only; idempotent on duplicate jti. backend/auth/wallet_auth.go: - tokenTTLs map: track 1 = 12h, 2 = 8h, 3 = 4h, 4 = 60m. tokenTTLFor returns the ceiling; default is 12h for unknown tracks. - generateJWT now embeds a 128-bit random jti (hex-encoded) and uses the per-track TTL instead of a hardcoded 24h. - parseJWT: shared signature-verification + claim-extraction helper used by ValidateJWT and RefreshJWT. Returns address, track, jti, exp. - jtiFromToken: parses jti from an already-trusted token without a second crypto roundtrip. - isJTIRevoked: EXISTS query against jwt_revocations, returning ErrJWTRevocationStorageMissing when the table is absent (migration not run yet) so callers can surface a 503 rather than silently treating every token as valid. - RevokeJWT(ctx, token, reason): records the jti; idempotent via ON CONFLICT (jti) DO NOTHING. Refuses legacy tokens without jti. - RefreshJWT(ctx, token): validates, revokes the old token (reason 'refresh'), and mints a new token with fresh jti + fresh TTL. Same (address, track) as the inbound token, same permissions set. - ValidateJWT now consults jwt_revocations when a DB is configured; returns ErrJWTRevoked for revoked tokens. backend/api/rest/auth_refresh.go (new): - POST /api/v1/auth/refresh handler: expects 'Authorization: Bearer <jwt>'; returns WalletAuthResponse with the new token. Maps ErrJWTRevoked to 401 token_revoked and ErrWalletAuthStorageNotInitialized to 503. - POST /api/v1/auth/logout handler: same header contract, idempotent, returns {status: ok}. Returns 503 when the revocations table isn't present so ops know migration 0016 hasn't run. - Both handlers reuse the existing extractBearerToken helper from auth.go so parsing is consistent with the rest of the access layer. backend/api/rest/routes.go: - Registered /api/v1/auth/refresh and /api/v1/auth/logout. Tests: - TestTokenTTLForTrack4IsShort: track 4 TTL <= 1h. - TestTokenTTLForTrack1Track2Track3AreReasonable: bounded at 12h. - TestGeneratedJWTCarriesJTIClaim: jti is present, 128 bits / 32 hex. - TestGeneratedJWTExpIsTrackAppropriate: exp matches tokenTTLFor per track within a couple-second tolerance. - TestRevokeJWTWithoutDBReturnsError: a WalletAuth with nil db must refuse to revoke rather than silently pretending it worked. - All pre-existing wallet_auth tests still pass. Also fixes a small SA4006/SA4017 regression in mission_control.go that PR #5 introduced by shadowing the outer err with json.Unmarshal's err return. Reworked to uerr so the outer err and the RPC fallback still function as intended. Verification: go build ./... clean go vet ./... clean go test ./auth/... PASS (including new tests) go test ./api/rest/... PASS staticcheck ./auth/... ./api/rest/... clean on SA4006/SA4017/SA1029 Advances completion criterion 3 (JWT hygiene): 'Track 4 sessions TTL <= 1h; server-side revocation list (keyed on jti) enforced on every token validation; refresh endpoint rotates the token in place so the short TTL is usable in practice; logout endpoint revokes immediately.'
2026-04-18 19:20:57 +00:00
mux.HandleFunc("/api/v1/auth/refresh", s.handleAuthRefresh)
mux.HandleFunc("/api/v1/auth/logout", s.handleAuthLogout)
mux.HandleFunc("/api/v1/walletconnect/", s.handleWalletConnectRoot)
mux.HandleFunc("/api/v1/walletconnect", s.handleWalletConnectRoot)
mux.HandleFunc("/api/v1/auth/register", s.handleAuthRegister)
mux.HandleFunc("/api/v1/auth/login", s.handleAuthLogin)
mux.HandleFunc("/api/v1/access/me", s.handleAccessMe)
mux.HandleFunc("/api/v1/access/products", s.handleAccessProducts)
mux.HandleFunc("/api/v1/access/subscriptions", s.handleAccessSubscriptions)
mux.HandleFunc("/api/v1/access/admin/subscriptions", s.handleAccessAdminSubscriptions)
mux.HandleFunc("/api/v1/access/admin/audit", s.handleAccessAdminAudit)
mux.HandleFunc("/api/v1/access/internal/validate-key", s.handleAccessInternalValidateAPIKey)
mux.HandleFunc("/api/v1/access/api-keys", s.handleAccessAPIKeys)
mux.HandleFunc("/api/v1/access/api-keys/", s.handleAccessAPIKeyAction)
mux.HandleFunc("/api/v1/access/usage", s.handleAccessUsage)
mux.HandleFunc("/api/v1/access/audit", s.handleAccessAudit)
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>
2026-05-09 20:32:06 +00:00
// 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)
// mux.HandleFunc("/api/v1/track1/txs/latest", s.track1Server.handleLatestTransactions)
// mux.HandleFunc("/api/v1/track1/block/", s.track1Server.handleBlockDetail)
// mux.HandleFunc("/api/v1/track1/tx/", s.track1Server.handleTransactionDetail)
// mux.HandleFunc("/api/v1/track1/address/", s.track1Server.handleAddressBalance)
// mux.HandleFunc("/api/v1/track1/bridge/status", s.track1Server.handleBridgeStatus)
// Track 2 routes (require Track 2+)
// Note: Track 2 endpoints should be registered with RequireAuth + RequireTrack(2) middleware
// mux.HandleFunc("/api/v1/track2/address/", s.track2Server.handleAddressTransactions)
// mux.HandleFunc("/api/v1/track2/token/", s.track2Server.handleTokenInfo)
// mux.HandleFunc("/api/v1/track2/search", s.track2Server.handleSearch)
// Track 3 routes (require Track 3+)
// Note: Track 3 endpoints should be registered with RequireAuth + RequireTrack(3) middleware
// mux.HandleFunc("/api/v1/track3/analytics/flows", s.track3Server.handleFlows)
// mux.HandleFunc("/api/v1/track3/analytics/bridge", s.track3Server.handleBridge)
// mux.HandleFunc("/api/v1/track3/analytics/token-distribution/", s.track3Server.handleTokenDistribution)
// mux.HandleFunc("/api/v1/track3/analytics/address-risk/", s.track3Server.handleAddressRisk)
// Track 4 routes (require Track 4)
// Note: Track 4 endpoints should be registered with RequireAuth + RequireTrack(4) + IP whitelist middleware
// mux.HandleFunc("/api/v1/track4/operator/bridge/events", s.track4Server.handleBridgeEvents)
// mux.HandleFunc("/api/v1/track4/operator/validators", s.track4Server.handleValidators)
// mux.HandleFunc("/api/v1/track4/operator/contracts", s.track4Server.handleContracts)
// mux.HandleFunc("/api/v1/track4/operator/protocol-state", s.track4Server.handleProtocolState)
}
// handleBlockDetail handles GET /api/v1/blocks/{chain_id}/{number} or /api/v1/blocks/{chain_id}/hash/{hash}
func (s *Server) handleBlockDetail(w http.ResponseWriter, r *http.Request) {
if !s.requireDB(w) {
return
}
path := strings.TrimPrefix(r.URL.Path, "/api/v1/blocks/")
parts := strings.Split(path, "/")
if len(parts) < 2 {
writeValidationError(w, fmt.Errorf("invalid block path"))
return
}
// Validate chain ID
if err := validateChainID(parts[0], s.chainID); err != nil {
writeValidationError(w, err)
return
}
if parts[1] == "hash" && len(parts) == 3 {
// Validate hash format
hash := normalizeHash(parts[2])
if !isValidHash(hash) {
writeValidationError(w, ErrInvalidHash)
return
}
// Get by hash
s.handleGetBlockByHash(w, r, hash)
} else {
// Validate and parse block number
blockNumber, err := validateBlockNumber(parts[1])
if err != nil {
writeValidationError(w, err)
return
}
s.handleGetBlockByNumber(w, r, blockNumber)
}
}
// handleGetBlockByNumber and handleGetBlockByHash are in blocks.go
// handleTransactionDetail handles GET /api/v1/transactions/{chain_id}/{hash}
func (s *Server) handleTransactionDetail(w http.ResponseWriter, r *http.Request) {
if !s.requireDB(w) {
return
}
path := strings.TrimPrefix(r.URL.Path, "/api/v1/transactions/")
parts := strings.Split(path, "/")
if len(parts) < 2 {
writeValidationError(w, fmt.Errorf("invalid transaction path"))
return
}
// Validate chain ID
if err := validateChainID(parts[0], s.chainID); err != nil {
writeValidationError(w, err)
return
}
// Validate hash format
hash := normalizeHash(parts[1])
if !isValidHash(hash) {
writeValidationError(w, ErrInvalidHash)
return
}
s.handleGetTransactionByHash(w, r, hash)
}
// handleGetTransactionByHash is implemented in transactions.go
// handleAddressDetail handles GET /api/v1/addresses/{chain_id}/{address}
func (s *Server) handleAddressDetail(w http.ResponseWriter, r *http.Request) {
if !s.requireDB(w) {
return
}
path := strings.TrimPrefix(r.URL.Path, "/api/v1/addresses/")
parts := strings.Split(path, "/")
if len(parts) < 2 {
writeValidationError(w, fmt.Errorf("invalid address path"))
return
}
// Validate chain ID
if err := validateChainID(parts[0], s.chainID); err != nil {
writeValidationError(w, err)
return
}
// Validate address format
address := normalizeAddress(parts[1])
if !isValidAddress(address) {
writeValidationError(w, ErrInvalidAddress)
return
}
// Set address in query and call handler
query := r.URL.Query()
query.Set("address", address)
r.URL.RawQuery = query.Encode()
s.handleGetAddress(w, r)
}