2026-02-10 11:32:49 -08:00
package rest
import (
"encoding/json"
2026-04-07 23:22:12 -07:00
"errors"
2026-04-10 12:52:17 -07:00
"fmt"
"io"
2026-02-10 11:32:49 -08:00
"net/http"
2026-04-10 12:52:17 -07:00
"os"
"strconv"
"strings"
"time"
2026-02-10 11:32:49 -08:00
"github.com/explorer/backend/auth"
2026-04-10 12:52:17 -07:00
"github.com/golang-jwt/jwt/v4"
2026-02-10 11:32:49 -08:00
)
// handleAuthNonce handles POST /api/v1/auth/nonce
func ( s * Server ) handleAuthNonce ( w http . ResponseWriter , r * http . Request ) {
if r . Method != http . MethodPost {
writeError ( w , http . StatusMethodNotAllowed , "method_not_allowed" , "Method not allowed" )
return
}
2026-04-07 23:22:12 -07:00
if ! s . requireDB ( w ) {
return
}
2026-02-10 11:32:49 -08:00
var req auth . NonceRequest
if err := json . NewDecoder ( r . Body ) . Decode ( & req ) ; err != nil {
writeError ( w , http . StatusBadRequest , "bad_request" , "Invalid request body" )
return
}
// Generate nonce
nonceResp , err := s . walletAuth . GenerateNonce ( r . Context ( ) , req . Address )
if err != nil {
2026-04-07 23:22:12 -07:00
if errors . Is ( err , auth . ErrWalletAuthStorageNotInitialized ) {
writeError ( w , http . StatusServiceUnavailable , "service_unavailable" , err . Error ( ) )
return
}
2026-02-10 11:32:49 -08:00
writeError ( w , http . StatusBadRequest , "bad_request" , err . Error ( ) )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
json . NewEncoder ( w ) . Encode ( nonceResp )
}
// handleAuthWallet handles POST /api/v1/auth/wallet
func ( s * Server ) handleAuthWallet ( w http . ResponseWriter , r * http . Request ) {
if r . Method != http . MethodPost {
writeError ( w , http . StatusMethodNotAllowed , "method_not_allowed" , "Method not allowed" )
return
}
2026-04-07 23:22:12 -07:00
if ! s . requireDB ( w ) {
return
}
2026-02-10 11:32:49 -08:00
var req auth . WalletAuthRequest
if err := json . NewDecoder ( r . Body ) . Decode ( & req ) ; err != nil {
writeError ( w , http . StatusBadRequest , "bad_request" , "Invalid request body" )
return
}
// Authenticate wallet
authResp , err := s . walletAuth . AuthenticateWallet ( r . Context ( ) , & req )
if err != nil {
2026-04-07 23:22:12 -07:00
if errors . Is ( err , auth . ErrWalletAuthStorageNotInitialized ) {
writeError ( w , http . StatusServiceUnavailable , "service_unavailable" , err . Error ( ) )
return
}
2026-02-10 11:32:49 -08:00
writeError ( w , http . StatusUnauthorized , "unauthorized" , err . Error ( ) )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
json . NewEncoder ( w ) . Encode ( authResp )
}
2026-04-10 12:52:17 -07:00
type userAuthRequest struct {
Email string ` json:"email" `
Username string ` json:"username" `
Password string ` json:"password" `
}
type accessProduct struct {
Slug string ` json:"slug" `
Name string ` json:"name" `
Provider string ` json:"provider" `
VMID int ` json:"vmid" `
HTTPURL string ` json:"http_url" `
WSURL string ` json:"ws_url,omitempty" `
DefaultTier string ` json:"default_tier" `
RequiresApproval bool ` json:"requires_approval" `
BillingModel string ` json:"billing_model" `
Description string ` json:"description" `
UseCases [ ] string ` json:"use_cases" `
ManagementFeatures [ ] string ` json:"management_features" `
}
type userSessionClaims struct {
UserID string ` json:"user_id" `
Email string ` json:"email" `
Username string ` json:"username" `
jwt . RegisteredClaims
}
type createAPIKeyRequest struct {
Name string ` json:"name" `
Tier string ` json:"tier" `
ProductSlug string ` json:"product_slug" `
ExpiresDays int ` json:"expires_days" `
MonthlyQuota int ` json:"monthly_quota" `
Scopes [ ] string ` json:"scopes" `
}
type createSubscriptionRequest struct {
ProductSlug string ` json:"product_slug" `
Tier string ` json:"tier" `
}
type accessUsageSummary struct {
ProductSlug string ` json:"product_slug" `
ActiveKeys int ` json:"active_keys" `
RequestsUsed int ` json:"requests_used" `
MonthlyQuota int ` json:"monthly_quota" `
}
type accessAuditEntry = auth . APIKeyUsageLog
type adminSubscriptionActionRequest struct {
SubscriptionID string ` json:"subscription_id" `
Status string ` json:"status" `
Notes string ` json:"notes" `
}
type internalValidateAPIKeyRequest struct {
APIKey string ` json:"api_key" `
MethodName string ` json:"method_name" `
RequestCount int ` json:"request_count" `
LastIP string ` json:"last_ip" `
}
var rpcAccessProducts = [ ] accessProduct {
{
Slug : "core-rpc" ,
Name : "Core RPC" ,
Provider : "besu-core" ,
VMID : 2101 ,
HTTPURL : "https://rpc-http-prv.d-bis.org" ,
WSURL : "wss://rpc-ws-prv.d-bis.org" ,
DefaultTier : "enterprise" ,
RequiresApproval : true ,
BillingModel : "contract" ,
Description : "Private Chain 138 Core RPC for operator-grade administration and sensitive workloads." ,
UseCases : [ ] string { "core deployments" , "operator automation" , "private infrastructure integration" } ,
ManagementFeatures : [ ] string { "dedicated API key" , "higher rate ceiling" , "operator-oriented access controls" } ,
} ,
{
Slug : "alltra-rpc" ,
Name : "Alltra RPC" ,
Provider : "alltra" ,
VMID : 2102 ,
HTTPURL : "http://192.168.11.212:8545" ,
WSURL : "ws://192.168.11.212:8546" ,
DefaultTier : "pro" ,
RequiresApproval : false ,
BillingModel : "subscription" ,
Description : "Dedicated Alltra-managed RPC lane for partner traffic, subscription access, and API-key-gated usage." ,
UseCases : [ ] string { "tenant RPC access" , "managed partner workloads" , "metered commercial usage" } ,
ManagementFeatures : [ ] string { "subscription-ready key issuance" , "rate governance" , "partner-specific traffic lane" } ,
} ,
{
Slug : "thirdweb-rpc" ,
Name : "Thirdweb RPC" ,
Provider : "thirdweb" ,
VMID : 2103 ,
HTTPURL : "http://192.168.11.217:8545" ,
WSURL : "ws://192.168.11.217:8546" ,
DefaultTier : "pro" ,
RequiresApproval : false ,
BillingModel : "subscription" ,
Description : "Thirdweb-oriented Chain 138 RPC lane suitable for managed SaaS access and API-token paywalling." ,
UseCases : [ ] string { "thirdweb integrations" , "commercial API access" , "managed dApp traffic" } ,
ManagementFeatures : [ ] string { "API token issuance" , "usage tiering" , "future paywall/subscription hooks" } ,
} ,
}
func ( s * Server ) generateUserJWT ( user * auth . User ) ( string , time . Time , error ) {
expiresAt := time . Now ( ) . Add ( 7 * 24 * time . Hour )
claims := userSessionClaims {
UserID : user . ID ,
Email : user . Email ,
Username : user . Username ,
RegisteredClaims : jwt . RegisteredClaims {
ExpiresAt : jwt . NewNumericDate ( expiresAt ) ,
IssuedAt : jwt . NewNumericDate ( time . Now ( ) ) ,
Subject : user . ID ,
} ,
}
token := jwt . NewWithClaims ( jwt . SigningMethodHS256 , claims )
tokenString , err := token . SignedString ( s . jwtSecret )
if err != nil {
return "" , time . Time { } , err
}
return tokenString , expiresAt , nil
}
func ( s * Server ) validateUserJWT ( tokenString string ) ( * userSessionClaims , error ) {
token , err := jwt . ParseWithClaims ( tokenString , & userSessionClaims { } , func ( token * jwt . Token ) ( interface { } , error ) {
if _ , ok := token . Method . ( * jwt . SigningMethodHMAC ) ; ! ok {
return nil , fmt . Errorf ( "unexpected signing method" )
}
return s . jwtSecret , nil
} )
if err != nil {
return nil , err
}
claims , ok := token . Claims . ( * userSessionClaims )
if ! ok || ! token . Valid {
return nil , fmt . Errorf ( "invalid token" )
}
return claims , nil
}
func extractBearerToken ( r * http . Request ) string {
authHeader := r . Header . Get ( "Authorization" )
if authHeader == "" {
return ""
}
parts := strings . SplitN ( authHeader , " " , 2 )
if len ( parts ) != 2 || ! strings . EqualFold ( parts [ 0 ] , "Bearer" ) {
return ""
}
return strings . TrimSpace ( parts [ 1 ] )
}
func ( s * Server ) requireUserSession ( w http . ResponseWriter , r * http . Request ) ( * userSessionClaims , bool ) {
token := extractBearerToken ( r )
if token == "" {
writeError ( w , http . StatusUnauthorized , "unauthorized" , "User session required" )
return nil , false
}
claims , err := s . validateUserJWT ( token )
if err != nil {
writeError ( w , http . StatusUnauthorized , "unauthorized" , "Invalid or expired session token" )
return nil , false
}
return claims , true
}
func isEmailInCSVAllowlist ( email string , raw string ) bool {
if strings . TrimSpace ( email ) == "" || strings . TrimSpace ( raw ) == "" {
return false
}
for _ , candidate := range strings . Split ( raw , "," ) {
if strings . EqualFold ( strings . TrimSpace ( candidate ) , strings . TrimSpace ( email ) ) {
return true
}
}
return false
}
func ( s * Server ) isAccessAdmin ( claims * userSessionClaims ) bool {
return claims != nil && isEmailInCSVAllowlist ( claims . Email , os . Getenv ( "ACCESS_ADMIN_EMAILS" ) )
}
func ( s * Server ) requireInternalAccessSecret ( w http . ResponseWriter , r * http . Request ) bool {
configured := strings . TrimSpace ( os . Getenv ( "ACCESS_INTERNAL_SECRET" ) )
if configured == "" {
writeError ( w , http . StatusServiceUnavailable , "service_unavailable" , "Internal access secret is not configured" )
return false
}
presented := strings . TrimSpace ( r . Header . Get ( "X-Access-Internal-Secret" ) )
if presented == "" || presented != configured {
writeError ( w , http . StatusUnauthorized , "unauthorized" , "Internal access secret required" )
return false
}
return true
}
func ( s * Server ) handleAuthRegister ( w http . ResponseWriter , r * http . Request ) {
if r . Method != http . MethodPost {
writeError ( w , http . StatusMethodNotAllowed , "method_not_allowed" , "Method not allowed" )
return
}
if ! s . requireDB ( w ) {
return
}
var req userAuthRequest
if err := json . NewDecoder ( r . Body ) . Decode ( & req ) ; err != nil {
writeError ( w , http . StatusBadRequest , "bad_request" , "Invalid request body" )
return
}
if strings . TrimSpace ( req . Email ) == "" || strings . TrimSpace ( req . Username ) == "" || len ( req . Password ) < 8 {
writeError ( w , http . StatusBadRequest , "bad_request" , "Email, username, and an 8+ character password are required" )
return
}
user , err := s . userAuth . RegisterUser ( r . Context ( ) , strings . TrimSpace ( req . Email ) , strings . TrimSpace ( req . Username ) , req . Password )
if err != nil {
writeError ( w , http . StatusBadRequest , "bad_request" , err . Error ( ) )
return
}
token , expiresAt , err := s . generateUserJWT ( user )
if err != nil {
writeError ( w , http . StatusInternalServerError , "internal_error" , "Failed to create session" )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ = json . NewEncoder ( w ) . Encode ( map [ string ] any {
"user" : map [ string ] any {
"id" : user . ID ,
"email" : user . Email ,
"username" : user . Username ,
} ,
"token" : token ,
"expires_at" : expiresAt ,
} )
}
func ( s * Server ) handleAuthLogin ( w http . ResponseWriter , r * http . Request ) {
if r . Method != http . MethodPost {
writeError ( w , http . StatusMethodNotAllowed , "method_not_allowed" , "Method not allowed" )
return
}
if ! s . requireDB ( w ) {
return
}
var req userAuthRequest
if err := json . NewDecoder ( r . Body ) . Decode ( & req ) ; err != nil {
writeError ( w , http . StatusBadRequest , "bad_request" , "Invalid request body" )
return
}
user , err := s . userAuth . AuthenticateUser ( r . Context ( ) , strings . TrimSpace ( req . Email ) , req . Password )
if err != nil {
writeError ( w , http . StatusUnauthorized , "unauthorized" , err . Error ( ) )
return
}
token , expiresAt , err := s . generateUserJWT ( user )
if err != nil {
writeError ( w , http . StatusInternalServerError , "internal_error" , "Failed to create session" )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ = json . NewEncoder ( w ) . Encode ( map [ string ] any {
"user" : map [ string ] any {
"id" : user . ID ,
"email" : user . Email ,
"username" : user . Username ,
} ,
"token" : token ,
"expires_at" : expiresAt ,
} )
}
func ( s * Server ) handleAccessProducts ( w http . ResponseWriter , r * http . Request ) {
if r . Method != http . MethodGet {
writeError ( w , http . StatusMethodNotAllowed , "method_not_allowed" , "Method not allowed" )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ = json . NewEncoder ( w ) . Encode ( map [ string ] any {
"products" : rpcAccessProducts ,
"note" : "Products are ready for auth, API key, and subscription gating. Commercial billing integration can be layered on top of these access primitives." ,
} )
}
func ( s * Server ) handleAccessMe ( w http . ResponseWriter , r * http . Request ) {
if r . Method != http . MethodGet {
writeError ( w , http . StatusMethodNotAllowed , "method_not_allowed" , "Method not allowed" )
return
}
claims , ok := s . requireUserSession ( w , r )
if ! ok {
return
}
subscriptions , _ := s . userAuth . ListSubscriptions ( r . Context ( ) , claims . UserID )
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ = json . NewEncoder ( w ) . Encode ( map [ string ] any {
"user" : map [ string ] any {
"id" : claims . UserID ,
"email" : claims . Email ,
"username" : claims . Username ,
"is_admin" : s . isAccessAdmin ( claims ) ,
} ,
"subscriptions" : subscriptions ,
} )
}
func ( s * Server ) handleAccessAPIKeys ( w http . ResponseWriter , r * http . Request ) {
if ! s . requireDB ( w ) {
return
}
claims , ok := s . requireUserSession ( w , r )
if ! ok {
return
}
switch r . Method {
case http . MethodGet :
keys , err := s . userAuth . ListAPIKeys ( r . Context ( ) , claims . UserID )
if err != nil {
writeError ( w , http . StatusInternalServerError , "internal_error" , err . Error ( ) )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ = json . NewEncoder ( w ) . Encode ( map [ string ] any { "api_keys" : keys } )
case http . MethodPost :
var req createAPIKeyRequest
if err := json . NewDecoder ( r . Body ) . Decode ( & req ) ; err != nil {
writeError ( w , http . StatusBadRequest , "bad_request" , "Invalid request body" )
return
}
if strings . TrimSpace ( req . Name ) == "" {
writeError ( w , http . StatusBadRequest , "bad_request" , "Key name is required" )
return
}
tier := strings . ToLower ( strings . TrimSpace ( req . Tier ) )
if tier == "" {
tier = "free"
}
productSlug := strings . TrimSpace ( req . ProductSlug )
product := findAccessProduct ( productSlug )
if productSlug != "" && product == nil {
writeError ( w , http . StatusBadRequest , "bad_request" , "Unknown product" )
return
}
subscriptions , err := s . userAuth . ListSubscriptions ( r . Context ( ) , claims . UserID )
if err != nil {
writeError ( w , http . StatusInternalServerError , "internal_error" , err . Error ( ) )
return
}
var subscriptionStatus string
for _ , subscription := range subscriptions {
if subscription . ProductSlug == productSlug {
subscriptionStatus = subscription . Status
break
}
}
if product != nil {
if subscriptionStatus == "" {
status := "active"
if product . RequiresApproval {
status = "pending"
}
_ , err := s . userAuth . UpsertProductSubscription (
r . Context ( ) ,
claims . UserID ,
productSlug ,
tier ,
status ,
defaultQuotaForTier ( tier ) ,
product . RequiresApproval ,
"" ,
"" ,
)
if err != nil {
writeError ( w , http . StatusInternalServerError , "internal_error" , err . Error ( ) )
return
}
subscriptionStatus = status
}
if subscriptionStatus != "active" {
writeError ( w , http . StatusForbidden , "subscription_required" , "Product access is pending approval or inactive" )
return
}
}
fullName := req . Name
if productSlug != "" {
fullName = fmt . Sprintf ( "%s [%s]" , req . Name , productSlug )
}
monthlyQuota := req . MonthlyQuota
if monthlyQuota <= 0 {
monthlyQuota = defaultQuotaForTier ( tier )
}
scopes := req . Scopes
if len ( scopes ) == 0 {
scopes = defaultScopesForProduct ( productSlug )
}
apiKey , err := s . userAuth . GenerateScopedAPIKey (
r . Context ( ) ,
claims . UserID ,
fullName ,
tier ,
productSlug ,
scopes ,
monthlyQuota ,
product == nil || ! product . RequiresApproval ,
req . ExpiresDays ,
)
if err != nil {
writeError ( w , http . StatusBadRequest , "bad_request" , err . Error ( ) )
return
}
keys , _ := s . userAuth . ListAPIKeys ( r . Context ( ) , claims . UserID )
var latest any
if len ( keys ) > 0 {
latest = keys [ 0 ]
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ = json . NewEncoder ( w ) . Encode ( map [ string ] any {
"api_key" : apiKey ,
"record" : latest ,
} )
default :
writeError ( w , http . StatusMethodNotAllowed , "method_not_allowed" , "Method not allowed" )
}
}
func ( s * Server ) handleAccessInternalValidateAPIKey ( w http . ResponseWriter , r * http . Request ) {
if ! s . requireDB ( w ) {
return
}
if r . Method != http . MethodPost && r . Method != http . MethodGet {
writeError ( w , http . StatusMethodNotAllowed , "method_not_allowed" , "Method not allowed" )
return
}
if ! s . requireInternalAccessSecret ( w , r ) {
return
}
req , err := parseInternalValidateAPIKeyRequest ( r )
if err != nil {
writeError ( w , http . StatusBadRequest , "bad_request" , err . Error ( ) )
return
}
if strings . TrimSpace ( req . APIKey ) == "" {
writeError ( w , http . StatusBadRequest , "bad_request" , "API key is required" )
return
}
info , err := s . userAuth . ValidateAPIKeyDetailed (
r . Context ( ) ,
strings . TrimSpace ( req . APIKey ) ,
strings . TrimSpace ( req . MethodName ) ,
req . RequestCount ,
strings . TrimSpace ( req . LastIP ) ,
)
if err != nil {
writeError ( w , http . StatusUnauthorized , "unauthorized" , err . Error ( ) )
return
}
w . Header ( ) . Set ( "X-Validated-Product" , info . ProductSlug )
w . Header ( ) . Set ( "X-Validated-Tier" , info . Tier )
w . Header ( ) . Set ( "X-Validated-User" , info . UserID )
w . Header ( ) . Set ( "X-Validated-Scopes" , strings . Join ( info . Scopes , "," ) )
if info . MonthlyQuota > 0 {
remaining := info . MonthlyQuota - info . RequestsUsed
if remaining < 0 {
remaining = 0
}
w . Header ( ) . Set ( "X-Quota-Remaining" , strconv . Itoa ( remaining ) )
}
if r . Method == http . MethodGet {
w . WriteHeader ( http . StatusOK )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ = json . NewEncoder ( w ) . Encode ( map [ string ] any {
"valid" : true ,
"key" : info ,
} )
}
func parseInternalValidateAPIKeyRequest ( r * http . Request ) ( internalValidateAPIKeyRequest , error ) {
var req internalValidateAPIKeyRequest
if r . Method == http . MethodGet {
req . APIKey = firstNonEmpty (
r . Header . Get ( "X-API-Key" ) ,
extractBearerToken ( r ) ,
r . URL . Query ( ) . Get ( "api_key" ) ,
)
req . MethodName = firstNonEmpty (
r . Header . Get ( "X-Access-Method" ) ,
r . URL . Query ( ) . Get ( "method_name" ) ,
r . Method ,
)
req . LastIP = firstNonEmpty (
r . Header . Get ( "X-Real-IP" ) ,
r . Header . Get ( "X-Forwarded-For" ) ,
r . URL . Query ( ) . Get ( "last_ip" ) ,
)
req . RequestCount = 1
if rawCount := firstNonEmpty ( r . Header . Get ( "X-Access-Request-Count" ) , r . URL . Query ( ) . Get ( "request_count" ) ) ; rawCount != "" {
parsed , err := strconv . Atoi ( strings . TrimSpace ( rawCount ) )
if err != nil {
return req , fmt . Errorf ( "invalid request_count" )
}
req . RequestCount = parsed
}
return req , nil
}
if err := json . NewDecoder ( r . Body ) . Decode ( & req ) ; err != nil {
if errors . Is ( err , io . EOF ) {
return req , fmt . Errorf ( "invalid request body" )
}
return req , fmt . Errorf ( "invalid request body" )
}
if strings . TrimSpace ( req . MethodName ) == "" {
req . MethodName = r . Method
}
return req , nil
}
func firstNonEmpty ( values ... string ) string {
for _ , value := range values {
trimmed := strings . TrimSpace ( value )
if trimmed != "" {
return trimmed
}
}
return ""
}
func findAccessProduct ( slug string ) * accessProduct {
for _ , product := range rpcAccessProducts {
if product . Slug == slug {
copy := product
return & copy
}
}
return nil
}
func defaultQuotaForTier ( tier string ) int {
switch tier {
case "enterprise" :
return 1000000
case "pro" :
return 100000
default :
return 10000
}
}
func defaultScopesForProduct ( productSlug string ) [ ] string {
switch productSlug {
case "core-rpc" :
return [ ] string { "rpc:read" , "rpc:write" , "rpc:admin" }
case "alltra-rpc" , "thirdweb-rpc" :
return [ ] string { "rpc:read" , "rpc:write" }
default :
return [ ] string { "rpc:read" }
}
}
func ( s * Server ) handleAccessSubscriptions ( w http . ResponseWriter , r * http . Request ) {
if ! s . requireDB ( w ) {
return
}
claims , ok := s . requireUserSession ( w , r )
if ! ok {
return
}
switch r . Method {
case http . MethodGet :
subscriptions , err := s . userAuth . ListSubscriptions ( r . Context ( ) , claims . UserID )
if err != nil {
writeError ( w , http . StatusInternalServerError , "internal_error" , err . Error ( ) )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ = json . NewEncoder ( w ) . Encode ( map [ string ] any { "subscriptions" : subscriptions } )
case http . MethodPost :
var req createSubscriptionRequest
if err := json . NewDecoder ( r . Body ) . Decode ( & req ) ; err != nil {
writeError ( w , http . StatusBadRequest , "bad_request" , "Invalid request body" )
return
}
product := findAccessProduct ( strings . TrimSpace ( req . ProductSlug ) )
if product == nil {
writeError ( w , http . StatusBadRequest , "bad_request" , "Unknown product" )
return
}
tier := strings . ToLower ( strings . TrimSpace ( req . Tier ) )
if tier == "" {
tier = product . DefaultTier
}
status := "active"
notes := "Self-service activation"
if product . RequiresApproval {
status = "pending"
notes = "Awaiting manual approval for restricted product"
}
subscription , err := s . userAuth . UpsertProductSubscription (
r . Context ( ) ,
claims . UserID ,
product . Slug ,
tier ,
status ,
defaultQuotaForTier ( tier ) ,
product . RequiresApproval ,
"" ,
notes ,
)
if err != nil {
writeError ( w , http . StatusInternalServerError , "internal_error" , err . Error ( ) )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ = json . NewEncoder ( w ) . Encode ( map [ string ] any { "subscription" : subscription } )
default :
writeError ( w , http . StatusMethodNotAllowed , "method_not_allowed" , "Method not allowed" )
}
}
func ( s * Server ) handleAccessAdminSubscriptions ( w http . ResponseWriter , r * http . Request ) {
if ! s . requireDB ( w ) {
return
}
claims , ok := s . requireUserSession ( w , r )
if ! ok {
return
}
if ! s . isAccessAdmin ( claims ) {
writeError ( w , http . StatusForbidden , "forbidden" , "Access admin privileges required" )
return
}
switch r . Method {
case http . MethodGet :
status := strings . TrimSpace ( r . URL . Query ( ) . Get ( "status" ) )
subscriptions , err := s . userAuth . ListAllSubscriptions ( r . Context ( ) , status )
if err != nil {
writeError ( w , http . StatusInternalServerError , "internal_error" , err . Error ( ) )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ = json . NewEncoder ( w ) . Encode ( map [ string ] any { "subscriptions" : subscriptions } )
case http . MethodPost :
var req adminSubscriptionActionRequest
if err := json . NewDecoder ( r . Body ) . Decode ( & req ) ; err != nil {
writeError ( w , http . StatusBadRequest , "bad_request" , "Invalid request body" )
return
}
status := strings . ToLower ( strings . TrimSpace ( req . Status ) )
switch status {
case "active" , "suspended" , "revoked" :
default :
writeError ( w , http . StatusBadRequest , "bad_request" , "Status must be active, suspended, or revoked" )
return
}
if strings . TrimSpace ( req . SubscriptionID ) == "" {
writeError ( w , http . StatusBadRequest , "bad_request" , "Subscription id is required" )
return
}
subscription , err := s . userAuth . UpdateSubscriptionStatus (
r . Context ( ) ,
strings . TrimSpace ( req . SubscriptionID ) ,
status ,
claims . Email ,
strings . TrimSpace ( req . Notes ) ,
)
if err != nil {
writeError ( w , http . StatusBadRequest , "bad_request" , err . Error ( ) )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ = json . NewEncoder ( w ) . Encode ( map [ string ] any { "subscription" : subscription } )
default :
writeError ( w , http . StatusMethodNotAllowed , "method_not_allowed" , "Method not allowed" )
}
}
func ( s * Server ) handleAccessUsage ( w http . ResponseWriter , r * http . Request ) {
if ! s . requireDB ( w ) {
return
}
claims , ok := s . requireUserSession ( w , r )
if ! ok {
return
}
if r . Method != http . MethodGet {
writeError ( w , http . StatusMethodNotAllowed , "method_not_allowed" , "Method not allowed" )
return
}
keys , err := s . userAuth . ListAPIKeys ( r . Context ( ) , claims . UserID )
if err != nil {
writeError ( w , http . StatusInternalServerError , "internal_error" , err . Error ( ) )
return
}
grouped := map [ string ] * accessUsageSummary { }
for _ , key := range keys {
slug := key . ProductSlug
if slug == "" {
slug = "unscoped"
}
if _ , ok := grouped [ slug ] ; ! ok {
grouped [ slug ] = & accessUsageSummary { ProductSlug : slug }
}
summary := grouped [ slug ]
if ! key . Revoked {
summary . ActiveKeys ++
}
summary . RequestsUsed += key . RequestsUsed
summary . MonthlyQuota += key . MonthlyQuota
}
summaries := make ( [ ] accessUsageSummary , 0 , len ( grouped ) )
for _ , summary := range grouped {
summaries = append ( summaries , * summary )
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ = json . NewEncoder ( w ) . Encode ( map [ string ] any { "usage" : summaries } )
}
func ( s * Server ) handleAccessAudit ( w http . ResponseWriter , r * http . Request ) {
if ! s . requireDB ( w ) {
return
}
claims , ok := s . requireUserSession ( w , r )
if ! ok {
return
}
if r . Method != http . MethodGet {
writeError ( w , http . StatusMethodNotAllowed , "method_not_allowed" , "Method not allowed" )
return
}
limit := 20
if rawLimit := strings . TrimSpace ( r . URL . Query ( ) . Get ( "limit" ) ) ; rawLimit != "" {
parsed , err := strconv . Atoi ( rawLimit )
if err != nil || parsed < 1 || parsed > 200 {
writeError ( w , http . StatusBadRequest , "bad_request" , "Limit must be between 1 and 200" )
return
}
limit = parsed
}
entries , err := s . userAuth . ListUsageLogs ( r . Context ( ) , claims . UserID , limit )
if err != nil {
writeError ( w , http . StatusInternalServerError , "internal_error" , err . Error ( ) )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ = json . NewEncoder ( w ) . Encode ( map [ string ] any { "entries" : entries } )
}
func ( s * Server ) handleAccessAdminAudit ( w http . ResponseWriter , r * http . Request ) {
if ! s . requireDB ( w ) {
return
}
claims , ok := s . requireUserSession ( w , r )
if ! ok {
return
}
if ! s . isAccessAdmin ( claims ) {
writeError ( w , http . StatusForbidden , "forbidden" , "Access admin privileges required" )
return
}
if r . Method != http . MethodGet {
writeError ( w , http . StatusMethodNotAllowed , "method_not_allowed" , "Method not allowed" )
return
}
limit := 50
if rawLimit := strings . TrimSpace ( r . URL . Query ( ) . Get ( "limit" ) ) ; rawLimit != "" {
parsed , err := strconv . Atoi ( rawLimit )
if err != nil || parsed < 1 || parsed > 500 {
writeError ( w , http . StatusBadRequest , "bad_request" , "Limit must be between 1 and 500" )
return
}
limit = parsed
}
productSlug := strings . TrimSpace ( r . URL . Query ( ) . Get ( "product" ) )
if productSlug != "" && findAccessProduct ( productSlug ) == nil {
writeError ( w , http . StatusBadRequest , "bad_request" , "Unknown product" )
return
}
entries , err := s . userAuth . ListAllUsageLogs ( r . Context ( ) , productSlug , limit )
if err != nil {
writeError ( w , http . StatusInternalServerError , "internal_error" , err . Error ( ) )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ = json . NewEncoder ( w ) . Encode ( map [ string ] any { "entries" : entries } )
}
func ( s * Server ) handleAccessAPIKeyAction ( w http . ResponseWriter , r * http . Request ) {
if ! s . requireDB ( w ) {
return
}
claims , ok := s . requireUserSession ( w , r )
if ! ok {
return
}
if r . Method != http . MethodPost && r . Method != http . MethodDelete {
writeError ( w , http . StatusMethodNotAllowed , "method_not_allowed" , "Method not allowed" )
return
}
path := strings . TrimPrefix ( r . URL . Path , "/api/v1/access/api-keys/" )
parts := strings . Split ( strings . Trim ( path , "/" ) , "/" )
if len ( parts ) == 0 || parts [ 0 ] == "" {
writeError ( w , http . StatusBadRequest , "bad_request" , "API key id is required" )
return
}
keyID := parts [ 0 ]
if err := s . userAuth . RevokeAPIKey ( r . Context ( ) , claims . UserID , keyID ) ; err != nil {
writeError ( w , http . StatusBadRequest , "bad_request" , err . Error ( ) )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ = json . NewEncoder ( w ) . Encode ( map [ string ] any {
"revoked" : true ,
"api_key_id" : keyID ,
} )
}