refactor: rename SolaceScanScout to Solace and update related configurations

- Updated branding from "SolaceScanScout" to "Solace" across various files including deployment scripts, API responses, and documentation.
- Changed default base URL for Playwright tests and updated security headers to reflect the new branding.
- Enhanced README and API documentation to include new authentication endpoints and product access details.

This refactor aligns the project branding and improves clarity in the API documentation.
This commit is contained in:
defiQUG
2026-04-10 12:52:17 -07:00
parent 6eef6b07f6
commit 0972178cc5
160 changed files with 13274 additions and 1061 deletions

View File

@@ -3,9 +3,16 @@ package rest
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/explorer/backend/auth"
"github.com/golang-jwt/jwt/v4"
)
// handleAuthNonce handles POST /api/v1/auth/nonce
@@ -69,3 +76,851 @@ func (s *Server) handleAuthWallet(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(authResp)
}
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,
})
}