Add authentication to protected CLI commands

This commit adds wallet-based authentication to protected CLI commands
by removing the manual auth command and automatically prompting for
credentials when needed. Protected commands will check for valid
credentials and trigger the auth
This commit is contained in:
anonpenguin 2025-08-20 12:51:54 +03:00
parent 076edf4208
commit 1fca8cb411
4 changed files with 469 additions and 190 deletions

View File

@ -6,8 +6,6 @@ import (
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"os"
"os/exec"
"strconv"
@ -88,8 +86,7 @@ func main() {
handlePeerID()
case "help", "--help", "-h":
showHelp()
case "auth":
handleAuth(args)
default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", command)
showHelp()
@ -192,6 +189,9 @@ func handleStatus() {
}
func handleQuery(sql string) {
// Ensure user is authenticated
_ = ensureAuthenticated()
client, err := createClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
@ -221,6 +221,9 @@ func handleStorage(args []string) {
os.Exit(1)
}
// Ensure user is authenticated
_ = ensureAuthenticated()
client, err := createClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
@ -290,6 +293,9 @@ func handlePubSub(args []string) {
os.Exit(1)
}
// Ensure user is authenticated
_ = ensureAuthenticated()
client, err := createClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
@ -365,164 +371,18 @@ func handlePubSub(args []string) {
}
}
// handleAuth launches a local webpage to perform wallet signature and obtain an API key.
// Usage: network-cli auth [--gateway <url>] [--namespace <ns>] [--wallet <evm_addr>] [--plan <free|premium>]
func handleAuth(args []string) {
// Defaults
gatewayURL := getenvDefault("GATEWAY_URL", "http://localhost:8080")
namespace := getenvDefault("GATEWAY_NAMESPACE", "default")
wallet := ""
plan := "free"
// ensureAuthenticated ensures the user has valid credentials for the gateway
// Returns the credentials or exits the program on failure
func ensureAuthenticated() *auth.Credentials {
gatewayURL := auth.GetDefaultGatewayURL()
// Parse simple flags
for i := 0; i < len(args); i++ {
switch args[i] {
case "--gateway":
if i+1 < len(args) {
gatewayURL = strings.TrimSpace(args[i+1])
i++
}
case "--namespace":
if i+1 < len(args) {
namespace = strings.TrimSpace(args[i+1])
i++
}
case "--wallet":
if i+1 < len(args) {
wallet = strings.TrimSpace(args[i+1])
i++
}
case "--plan":
if i+1 < len(args) {
plan = strings.TrimSpace(strings.ToLower(args[i+1]))
i++
}
}
}
// Spin up local HTTP server on random port
ln, err := net.Listen("tcp", "localhost:0")
credentials, err := auth.GetOrPromptForCredentials(gatewayURL)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to listen: %v\n", err)
fmt.Fprintf(os.Stderr, "Authentication failed: %v\n", err)
os.Exit(1)
}
defer ln.Close()
addr := ln.Addr().String()
// Normalize URL host to localhost for consistency with gateway default
parts := strings.Split(addr, ":")
listenURL := "http://localhost:" + parts[len(parts)-1] + "/"
// Channel to receive API key
type result struct {
APIKey string `json:"api_key"`
Namespace string `json:"namespace"`
}
resCh := make(chan result, 1)
srv := &http.Server{}
mux := http.NewServeMux()
// Root serves the HTML page with embedded gateway URL and defaults
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, `<!doctype html>
<html>
<head><meta charset="utf-8"><title>DeBros Auth</title>
<style>body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;margin:2rem;max-width:720px}input,button,select{font-size:1rem;padding:.5rem;margin:.25rem 0}code{background:#f5f5f5;padding:.2rem .4rem;border-radius:4px}</style>
</head>
<body>
<h2>Authenticate with Wallet to Get API Key</h2>
<p>This will create or return an API key for namespace <code id="ns"></code> on gateway <code id="gw"></code>.</p>
<label>Wallet Address</label><br>
<input id="wallet" placeholder="0x..." style="width:100%%"/><br>
<label>Plan</label><br>
<select id="plan"><option value="free">free</option><option value="premium">premium (0.1 ETH)</option></select><br>
<button id="connect">Connect Wallet</button>
<button id="sign">Sign & Generate API Key</button>
<pre id="out" style="white-space:pre-wrap"></pre>
<script>
const GATEWAY = %q;
const DEFAULT_NS = %q;
const DEFAULT_WALLET = %q;
document.getElementById('gw').textContent = GATEWAY;
document.getElementById('ns').textContent = DEFAULT_NS;
document.getElementById('wallet').value = DEFAULT_WALLET;
document.getElementById('plan').value = %q;
const out = document.getElementById('out');
function log(m){ out.textContent += m + "\n" }
document.getElementById('connect').onclick = async () => {
if (!window.ethereum) { log('No wallet provider found (window.ethereum). Install MetaMask.'); return }
try { await window.ethereum.request({ method:'eth_requestAccounts' }); log('Wallet connected.'); } catch(e){ log('Connect failed: '+e.message) }
};
document.getElementById('sign').onclick = async () => {
try {
const wallet = document.getElementById('wallet').value.trim();
const plan = document.getElementById('plan').value;
if (!/^0x[0-9a-fA-F]{40}$/.test(wallet)) { log('Enter a valid EVM address'); return }
// Request nonce
const ch = await fetch(GATEWAY+"/v1/auth/challenge", {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({wallet, purpose:'api_key', namespace: DEFAULT_NS})});
if (!ch.ok) { const t = await ch.text(); log('Challenge failed: '+t); return }
const cj = await ch.json();
const nonce = cj.nonce;
// Sign nonce
let sig = await window.ethereum.request({ method:'personal_sign', params:[ nonce, wallet ] });
// Issue or fetch API key
const resp = await fetch(GATEWAY+"/v1/auth/api-key", {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({wallet, nonce, signature: sig, namespace: DEFAULT_NS, plan})});
if (!resp.ok) { const t = await resp.text(); log('Issue API key failed: '+t); return }
const data = await resp.json();
log('API Key: '+data.api_key+'\nNamespace: '+data.namespace);
// Send back to CLI
await fetch('/callback', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(data)});
} catch(e){ log('Error: '+e.message) }
};
</script>
</body></html>`, gatewayURL, namespace, wallet, plan)
})
// Callback to deliver API key back to CLI
mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
var payload struct {
APIKey string `json:"api_key"`
Namespace string `json:"namespace"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
if strings.TrimSpace(payload.APIKey) == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
select {
case resCh <- result{APIKey: payload.APIKey, Namespace: payload.Namespace}:
default:
}
_, _ = w.Write([]byte("ok"))
go func() { time.Sleep(500 * time.Millisecond); _ = srv.Close() }()
})
srv.Handler = mux
// Open browser
url := listenURL
go func() {
// Try to open in default browser
_ = openBrowser(url)
}()
// Serve and wait for result or timeout
go func() { _ = srv.Serve(ln) }()
fmt.Printf("🌐 Please complete authentication in your browser: %s\n", url)
select {
case r := <-resCh:
fmt.Printf("✅ API Key issued for namespace '%s'\n", r.Namespace)
fmt.Printf("%s\n", r.APIKey)
case <-time.After(5 * time.Minute):
fmt.Fprintf(os.Stderr, "Timed out waiting for wallet signature.\n")
_ = srv.Close()
os.Exit(1)
}
return credentials
}
func openBrowser(target string) error {
@ -737,26 +597,31 @@ func isPrintableText(s string) bool {
func showHelp() {
fmt.Printf("Network CLI - Distributed P2P Network Management Tool\n\n")
fmt.Printf("Usage: network-cli <command> [args...]\n\n")
fmt.Printf("🔐 Authentication: Commands requiring authentication will automatically prompt for wallet connection.\n\n")
fmt.Printf("Commands:\n")
fmt.Printf(" health - Check network health\n")
fmt.Printf(" peers - List connected peers\n")
fmt.Printf(" status - Show network status\n")
fmt.Printf(" peer-id - Show this node's peer ID\n")
fmt.Printf(" query <sql> - Execute database query\n")
fmt.Printf(" storage get <key> - Get value from storage\n")
fmt.Printf(" storage put <key> <value> - Store value in storage\n")
fmt.Printf(" storage list [prefix] - List storage keys\n")
fmt.Printf(" pubsub publish <topic> <msg> - Publish message\n")
fmt.Printf(" pubsub subscribe <topic> [duration] - Subscribe to topic\n")
fmt.Printf(" pubsub topics - List topics\n")
fmt.Printf(" query <sql> 🔐 Execute database query\n")
fmt.Printf(" storage get <key> 🔐 Get value from storage\n")
fmt.Printf(" storage put <key> <value> 🔐 Store value in storage\n")
fmt.Printf(" storage list [prefix] 🔐 List storage keys\n")
fmt.Printf(" pubsub publish <topic> <msg> 🔐 Publish message\n")
fmt.Printf(" pubsub subscribe <topic> [duration] 🔐 Subscribe to topic\n")
fmt.Printf(" pubsub topics 🔐 List topics\n")
fmt.Printf(" connect <peer_address> - Connect to peer\n")
fmt.Printf(" auth [--gateway URL] [--namespace NS] [--wallet 0x..] [--plan free|premium] - Obtain API key via wallet signature\n")
fmt.Printf(" help - Show this help\n\n")
fmt.Printf("Global Flags:\n")
fmt.Printf(" -b, --bootstrap <addr> - Bootstrap peer address (default: /ip4/127.0.0.1/tcp/4001)\n")
fmt.Printf(" -f, --format <format> - Output format: table, json (default: table)\n")
fmt.Printf(" -t, --timeout <duration> - Operation timeout (default: 30s)\n")
fmt.Printf(" --production - Connect to production bootstrap peers\n\n")
fmt.Printf("Authentication:\n")
fmt.Printf(" Commands marked with 🔐 will automatically prompt for wallet authentication\n")
fmt.Printf(" if no valid credentials are found. You can manage multiple wallets and\n")
fmt.Printf(" choose between them during the authentication flow.\n\n")
fmt.Printf("Examples:\n")
fmt.Printf(" network-cli health\n")
fmt.Printf(" network-cli peer-id\n")

395
pkg/auth/enhanced_auth.go Normal file
View File

@ -0,0 +1,395 @@
package auth
import (
"bufio"
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
)
// EnhancedCredentialStore manages multiple credentials per gateway
type EnhancedCredentialStore struct {
Gateways map[string]*GatewayCredentials `json:"gateways"`
Version string `json:"version"`
}
// GatewayCredentials holds multiple credentials for a single gateway
type GatewayCredentials struct {
Credentials []*Credentials `json:"credentials"`
DefaultIndex int `json:"default_index"`
LastUsedIndex int `json:"last_used_index"`
}
// AuthChoice represents user's choice during authentication
type AuthChoice int
const (
AuthChoiceUseCredential AuthChoice = iota
AuthChoiceAddCredential
AuthChoiceLogout
AuthChoiceExit
)
// LoadEnhancedCredentials loads the enhanced credential store
func LoadEnhancedCredentials() (*EnhancedCredentialStore, error) {
credPath, err := GetCredentialsPath()
if err != nil {
return nil, err
}
// If file doesn't exist, return empty store
if _, err := os.Stat(credPath); os.IsNotExist(err) {
return &EnhancedCredentialStore{
Gateways: make(map[string]*GatewayCredentials),
Version: "2.0",
}, nil
}
data, err := os.ReadFile(credPath)
if err != nil {
return nil, fmt.Errorf("failed to read credentials file: %w", err)
}
// Try to parse as enhanced store first
var enhancedStore EnhancedCredentialStore
if err := json.Unmarshal(data, &enhancedStore); err == nil && enhancedStore.Version == "2.0" {
// Initialize maps if nil
if enhancedStore.Gateways == nil {
enhancedStore.Gateways = make(map[string]*GatewayCredentials)
}
return &enhancedStore, nil
}
// Fall back to old format and migrate
var oldStore CredentialStore
if err := json.Unmarshal(data, &oldStore); err != nil {
return nil, fmt.Errorf("failed to parse credentials file: %w", err)
}
// Migrate old format to new
enhancedStore = EnhancedCredentialStore{
Gateways: make(map[string]*GatewayCredentials),
Version: "2.0",
}
for gatewayURL, creds := range oldStore.Gateways {
if creds != nil {
enhancedStore.Gateways[gatewayURL] = &GatewayCredentials{
Credentials: []*Credentials{creds},
DefaultIndex: 0,
LastUsedIndex: 0,
}
}
}
return &enhancedStore, nil
}
// Save saves the enhanced credential store
func (store *EnhancedCredentialStore) Save() error {
credPath, err := GetCredentialsPath()
if err != nil {
return err
}
if store.Version == "" {
store.Version = "2.0"
}
data, err := json.MarshalIndent(store, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal credentials: %w", err)
}
return os.WriteFile(credPath, data, 0600)
}
// AddCredential adds a new credential for the gateway
func (store *EnhancedCredentialStore) AddCredential(gatewayURL string, creds *Credentials) {
if store.Gateways == nil {
store.Gateways = make(map[string]*GatewayCredentials)
}
gatewayCredentials := store.Gateways[gatewayURL]
if gatewayCredentials == nil {
gatewayCredentials = &GatewayCredentials{
Credentials: []*Credentials{},
DefaultIndex: 0,
LastUsedIndex: 0,
}
store.Gateways[gatewayURL] = gatewayCredentials
}
// Check if credential already exists (by wallet address)
for i, existing := range gatewayCredentials.Credentials {
if strings.EqualFold(existing.Wallet, creds.Wallet) {
// Update existing credential
gatewayCredentials.Credentials[i] = creds
return
}
}
// Add new credential
gatewayCredentials.Credentials = append(gatewayCredentials.Credentials, creds)
}
// GetDefaultCredential returns the default credential for a gateway
func (store *EnhancedCredentialStore) GetDefaultCredential(gatewayURL string) *Credentials {
gatewayCredentials := store.Gateways[gatewayURL]
if gatewayCredentials == nil || len(gatewayCredentials.Credentials) == 0 {
return nil
}
// Ensure default index is valid
if gatewayCredentials.DefaultIndex < 0 || gatewayCredentials.DefaultIndex >= len(gatewayCredentials.Credentials) {
gatewayCredentials.DefaultIndex = 0
}
return gatewayCredentials.Credentials[gatewayCredentials.DefaultIndex]
}
// SetDefaultCredential sets the default credential by index
func (store *EnhancedCredentialStore) SetDefaultCredential(gatewayURL string, index int) bool {
gatewayCredentials := store.Gateways[gatewayURL]
if gatewayCredentials == nil || index < 0 || index >= len(gatewayCredentials.Credentials) {
return false
}
gatewayCredentials.DefaultIndex = index
gatewayCredentials.LastUsedIndex = index
return true
}
// ClearAllCredentials removes all credentials
func (store *EnhancedCredentialStore) ClearAllCredentials() {
store.Gateways = make(map[string]*GatewayCredentials)
}
// DisplayCredentialMenu shows the interactive credential selection menu
func (store *EnhancedCredentialStore) DisplayCredentialMenu(gatewayURL string) (AuthChoice, int, error) {
gatewayCredentials := store.Gateways[gatewayURL]
if gatewayCredentials == nil || len(gatewayCredentials.Credentials) == 0 {
fmt.Println("\n🔐 No credentials found. Choose an option:")
fmt.Println("1. Authenticate with new wallet")
fmt.Println("2. Exit")
fmt.Print("Choose (1-2): ")
choice, err := readUserChoice(2)
if err != nil {
return AuthChoiceExit, -1, err
}
switch choice {
case 1:
return AuthChoiceAddCredential, -1, nil
case 2:
return AuthChoiceExit, -1, nil
default:
return AuthChoiceExit, -1, fmt.Errorf("invalid choice")
}
}
fmt.Printf("\n🔐 Multiple wallets available for %s:\n", gatewayURL)
// Display credentials
for i, creds := range gatewayCredentials.Credentials {
defaultMark := ""
if i == gatewayCredentials.DefaultIndex {
defaultMark = " (default)"
}
// Format wallet address for display
displayAddr := creds.Wallet
if len(displayAddr) > 10 {
displayAddr = displayAddr[:6] + "..." + displayAddr[len(displayAddr)-4:]
}
statusEmoji := "✅"
if !creds.IsValid() {
statusEmoji = "❌"
}
planInfo := ""
if creds.Plan != "" {
planInfo = fmt.Sprintf(" (%s)", creds.Plan)
}
fmt.Printf("%d. %s %s%s%s\n", i+1, statusEmoji, displayAddr, planInfo, defaultMark)
}
fmt.Printf("%d. Add new wallet\n", len(gatewayCredentials.Credentials)+1)
fmt.Printf("%d. Logout (clear all credentials)\n", len(gatewayCredentials.Credentials)+2)
fmt.Printf("%d. Exit\n", len(gatewayCredentials.Credentials)+3)
maxChoice := len(gatewayCredentials.Credentials) + 3
fmt.Printf("Choose (1-%d): ", maxChoice)
choice, err := readUserChoice(maxChoice)
if err != nil {
return AuthChoiceExit, -1, err
}
if choice <= len(gatewayCredentials.Credentials) {
// User selected a credential
return AuthChoiceUseCredential, choice - 1, nil
} else if choice == len(gatewayCredentials.Credentials)+1 {
// Add new credential
return AuthChoiceAddCredential, -1, nil
} else if choice == len(gatewayCredentials.Credentials)+2 {
// Logout
return AuthChoiceLogout, -1, nil
} else {
// Exit
return AuthChoiceExit, -1, nil
}
}
// readUserChoice reads and validates user input
func readUserChoice(maxChoice int) (int, error) {
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
return 0, fmt.Errorf("failed to read input: %w", err)
}
choiceStr := strings.TrimSpace(input)
choice, err := strconv.Atoi(choiceStr)
if err != nil {
return 0, fmt.Errorf("invalid input: please enter a number")
}
if choice < 1 || choice > maxChoice {
return 0, fmt.Errorf("invalid choice: please enter a number between 1 and %d", maxChoice)
}
return choice, nil
}
// GetOrPromptForCredentials handles the complete authentication flow
func GetOrPromptForCredentials(gatewayURL string) (*Credentials, error) {
store, err := LoadEnhancedCredentials()
if err != nil {
return nil, fmt.Errorf("failed to load credential store: %w", err)
}
// Check if we have a valid default credential
defaultCreds := store.GetDefaultCredential(gatewayURL)
if defaultCreds != nil && defaultCreds.IsValid() {
// Update last used time
defaultCreds.UpdateLastUsed()
if err := store.Save(); err != nil {
// Log warning but don't fail
fmt.Fprintf(os.Stderr, "Warning: failed to update last used time: %v\n", err)
}
return defaultCreds, nil
}
// Need to prompt user for credential selection
for {
choice, credIndex, err := store.DisplayCredentialMenu(gatewayURL)
if err != nil {
return nil, fmt.Errorf("menu selection failed: %w", err)
}
switch choice {
case AuthChoiceUseCredential:
gatewayCredentials := store.Gateways[gatewayURL]
if gatewayCredentials == nil || credIndex < 0 || credIndex >= len(gatewayCredentials.Credentials) {
fmt.Println("❌ Invalid credential selection")
continue
}
selectedCreds := gatewayCredentials.Credentials[credIndex]
if !selectedCreds.IsValid() {
fmt.Println("❌ Selected credentials are invalid or expired")
continue
}
// Update default and last used
store.SetDefaultCredential(gatewayURL, credIndex)
selectedCreds.UpdateLastUsed()
if err := store.Save(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to save credentials: %v\n", err)
}
return selectedCreds, nil
case AuthChoiceAddCredential:
fmt.Println("\n🌐 Opening browser for wallet authentication...")
newCreds, err := PerformWalletAuthentication(gatewayURL)
if err != nil {
fmt.Printf("❌ Authentication failed: %v\n", err)
continue
}
// Add the new credential
store.AddCredential(gatewayURL, newCreds)
// Set as default if it's the first credential
gatewayCredentials := store.Gateways[gatewayURL]
if gatewayCredentials != nil && len(gatewayCredentials.Credentials) == 1 {
store.SetDefaultCredential(gatewayURL, 0)
}
if err := store.Save(); err != nil {
return nil, fmt.Errorf("failed to save new credentials: %w", err)
}
fmt.Printf("✅ Wallet %s added successfully\n", newCreds.Wallet)
return newCreds, nil
case AuthChoiceLogout:
store.ClearAllCredentials()
if err := store.Save(); err != nil {
return nil, fmt.Errorf("failed to clear credentials: %w", err)
}
fmt.Println("✅ All credentials cleared")
continue
case AuthChoiceExit:
return nil, fmt.Errorf("authentication cancelled by user")
default:
fmt.Println("❌ Invalid choice")
continue
}
}
}
// HasValidEnhancedCredentials checks if there are valid credentials for the default gateway
func HasValidEnhancedCredentials() (bool, error) {
store, err := LoadEnhancedCredentials()
if err != nil {
return false, err
}
gatewayURL := GetDefaultGatewayURL()
defaultCreds := store.GetDefaultCredential(gatewayURL)
return defaultCreds != nil && defaultCreds.IsValid(), nil
}
// GetValidEnhancedCredentials returns valid credentials for the default gateway
func GetValidEnhancedCredentials() (*Credentials, error) {
store, err := LoadEnhancedCredentials()
if err != nil {
return nil, err
}
gatewayURL := GetDefaultGatewayURL()
defaultCreds := store.GetDefaultCredential(gatewayURL)
if defaultCreds == nil {
return nil, fmt.Errorf("no credentials found for gateway %s", gatewayURL)
}
if !defaultCreds.IsValid() {
return nil, fmt.Errorf("credentials for gateway %s are expired or invalid", gatewayURL)
}
return defaultCreds, nil
}

View File

@ -11,6 +11,7 @@ import (
"strings"
"time"
"git.debros.io/DeBros/network/pkg/client"
"git.debros.io/DeBros/network/pkg/storage"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
)
@ -97,12 +98,14 @@ func (g *Gateway) challengeHandler(w http.ResponseWriter, r *http.Request) {
// Insert namespace if missing, fetch id
ctx := r.Context()
// Use internal context to bypass authentication for system operations
internalCtx := client.WithInternalAuth(ctx)
db := g.client.Database()
if _, err := db.Query(ctx, "INSERT OR IGNORE INTO namespaces(name) VALUES (?)", ns); err != nil {
if _, err := db.Query(internalCtx, "INSERT OR IGNORE INTO namespaces(name) VALUES (?)", ns); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
nres, err := db.Query(ctx, "SELECT id FROM namespaces WHERE name = ? LIMIT 1", ns)
nres, err := db.Query(internalCtx, "SELECT id FROM namespaces WHERE name = ? LIMIT 1", ns)
if err != nil || nres == nil || nres.Count == 0 || len(nres.Rows) == 0 || len(nres.Rows[0]) == 0 {
writeError(w, http.StatusInternalServerError, "failed to resolve namespace")
return
@ -110,7 +113,7 @@ func (g *Gateway) challengeHandler(w http.ResponseWriter, r *http.Request) {
nsID := nres.Rows[0][0]
// Store nonce with 5 minute expiry
if _, err := db.Query(ctx,
if _, err := db.Query(internalCtx,
"INSERT INTO nonces(namespace_id, wallet, nonce, purpose, expires_at) VALUES (?, ?, ?, ?, datetime('now', '+5 minutes'))",
nsID, req.Wallet, nonce, req.Purpose,
); err != nil {
@ -158,6 +161,8 @@ func (g *Gateway) verifyHandler(w http.ResponseWriter, r *http.Request) {
}
}
ctx := r.Context()
// Use internal context to bypass authentication for system operations
internalCtx := client.WithInternalAuth(ctx)
db := g.client.Database()
nsID, err := g.resolveNamespaceID(ctx, ns)
if err != nil {
@ -165,7 +170,7 @@ func (g *Gateway) verifyHandler(w http.ResponseWriter, r *http.Request) {
return
}
q := "SELECT id FROM nonces WHERE namespace_id = ? AND wallet = ? AND nonce = ? AND used_at IS NULL AND (expires_at IS NULL OR expires_at > datetime('now')) LIMIT 1"
nres, err := db.Query(ctx, q, nsID, req.Wallet, req.Nonce)
nres, err := db.Query(internalCtx, q, nsID, req.Wallet, req.Nonce)
if err != nil || nres == nil || nres.Count == 0 {
writeError(w, http.StatusBadRequest, "invalid or expired nonce")
return
@ -206,7 +211,7 @@ func (g *Gateway) verifyHandler(w http.ResponseWriter, r *http.Request) {
}
// Mark nonce used now (after successful verification)
if _, err := db.Query(ctx, "UPDATE nonces SET used_at = datetime('now') WHERE id = ?", nonceID); err != nil {
if _, err := db.Query(internalCtx, "UPDATE nonces SET used_at = datetime('now') WHERE id = ?", nonceID); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
@ -227,7 +232,7 @@ func (g *Gateway) verifyHandler(w http.ResponseWriter, r *http.Request) {
return
}
refresh := base64.RawURLEncoding.EncodeToString(rbuf)
if _, err := db.Query(ctx, "INSERT INTO refresh_tokens(namespace_id, subject, token, audience, expires_at) VALUES (?, ?, ?, ?, datetime('now', '+30 days'))", nsID, req.Wallet, refresh, "gateway"); err != nil {
if _, err := db.Query(internalCtx, "INSERT INTO refresh_tokens(namespace_id, subject, token, audience, expires_at) VALUES (?, ?, ?, ?, datetime('now', '+30 days'))", nsID, req.Wallet, refresh, "gateway"); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
@ -282,6 +287,8 @@ func (g *Gateway) issueAPIKeyHandler(w http.ResponseWriter, r *http.Request) {
}
}
ctx := r.Context()
// Use internal context to bypass authentication for system operations
internalCtx := client.WithInternalAuth(ctx)
db := g.client.Database()
// Resolve namespace id
nsID, err := g.resolveNamespaceID(ctx, ns)
@ -291,7 +298,7 @@ func (g *Gateway) issueAPIKeyHandler(w http.ResponseWriter, r *http.Request) {
}
// Validate nonce exists and not used/expired
q := "SELECT id FROM nonces WHERE namespace_id = ? AND wallet = ? AND nonce = ? AND used_at IS NULL AND (expires_at IS NULL OR expires_at > datetime('now')) LIMIT 1"
nres, err := db.Query(ctx, q, nsID, req.Wallet, req.Nonce)
nres, err := db.Query(internalCtx, q, nsID, req.Wallet, req.Nonce)
if err != nil || nres == nil || nres.Count == 0 {
writeError(w, http.StatusBadRequest, "invalid or expired nonce")
return
@ -326,13 +333,13 @@ func (g *Gateway) issueAPIKeyHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Mark nonce used
if _, err := db.Query(ctx, "UPDATE nonces SET used_at = datetime('now') WHERE id = ?", nonceID); err != nil {
if _, err := db.Query(internalCtx, "UPDATE nonces SET used_at = datetime('now') WHERE id = ?", nonceID); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
// Check if api key exists for (namespace, wallet) via linkage table
var apiKey string
r1, err := db.Query(ctx, "SELECT api_keys.key FROM wallet_api_keys JOIN api_keys ON wallet_api_keys.api_key_id = api_keys.id WHERE wallet_api_keys.namespace_id = ? AND LOWER(wallet_api_keys.wallet) = LOWER(?) LIMIT 1", nsID, req.Wallet)
r1, err := db.Query(internalCtx, "SELECT api_keys.key FROM wallet_api_keys JOIN api_keys ON wallet_api_keys.api_key_id = api_keys.id WHERE wallet_api_keys.namespace_id = ? AND LOWER(wallet_api_keys.wallet) = LOWER(?) LIMIT 1", nsID, req.Wallet)
if err == nil && r1 != nil && r1.Count > 0 && len(r1.Rows) > 0 && len(r1.Rows[0]) > 0 {
if s, ok := r1.Rows[0][0].(string); ok {
apiKey = s
@ -349,21 +356,21 @@ func (g *Gateway) issueAPIKeyHandler(w http.ResponseWriter, r *http.Request) {
return
}
apiKey = "ak_" + base64.RawURLEncoding.EncodeToString(buf) + ":" + ns
if _, err := db.Query(ctx, "INSERT INTO api_keys(key, name, namespace_id) VALUES (?, ?, ?)", apiKey, "", nsID); err != nil {
if _, err := db.Query(internalCtx, "INSERT INTO api_keys(key, name, namespace_id) VALUES (?, ?, ?)", apiKey, "", nsID); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
// Create linkage
// Find api_key id
rid, err := db.Query(ctx, "SELECT id FROM api_keys WHERE key = ? LIMIT 1", apiKey)
rid, err := db.Query(internalCtx, "SELECT id FROM api_keys WHERE key = ? LIMIT 1", apiKey)
if err == nil && rid != nil && rid.Count > 0 && len(rid.Rows) > 0 && len(rid.Rows[0]) > 0 {
apiKeyID := rid.Rows[0][0]
_, _ = db.Query(ctx, "INSERT OR IGNORE INTO wallet_api_keys(namespace_id, wallet, api_key_id) VALUES (?, ?, ?)", nsID, strings.ToLower(req.Wallet), apiKeyID)
_, _ = db.Query(internalCtx, "INSERT OR IGNORE INTO wallet_api_keys(namespace_id, wallet, api_key_id) VALUES (?, ?, ?)", nsID, strings.ToLower(req.Wallet), apiKeyID)
}
}
// Record ownerships (best-effort)
_, _ = db.Query(ctx, "INSERT OR IGNORE INTO namespace_ownership(namespace_id, owner_type, owner_id) VALUES (?, 'api_key', ?)", nsID, apiKey)
_, _ = db.Query(ctx, "INSERT OR IGNORE INTO namespace_ownership(namespace_id, owner_type, owner_id) VALUES (?, 'wallet', ?)", nsID, req.Wallet)
_, _ = db.Query(internalCtx, "INSERT OR IGNORE INTO namespace_ownership(namespace_id, owner_type, owner_id) VALUES (?, 'api_key', ?)", nsID, apiKey)
_, _ = db.Query(internalCtx, "INSERT OR IGNORE INTO namespace_ownership(namespace_id, owner_type, owner_id) VALUES (?, 'wallet', ?)", nsID, req.Wallet)
writeJSON(w, http.StatusOK, map[string]any{
"api_key": apiKey,
@ -399,8 +406,10 @@ func (g *Gateway) apiKeyToJWTHandler(w http.ResponseWriter, r *http.Request) {
// Validate and get namespace
db := g.client.Database()
ctx := r.Context()
// Use internal context to bypass authentication for system operations
internalCtx := client.WithInternalAuth(ctx)
q := "SELECT namespaces.name FROM api_keys JOIN namespaces ON api_keys.namespace_id = namespaces.id WHERE api_keys.key = ? LIMIT 1"
res, err := db.Query(ctx, q, key)
res, err := db.Query(internalCtx, q, key)
if err != nil || res == nil || res.Count == 0 || len(res.Rows) == 0 || len(res.Rows[0]) == 0 {
writeError(w, http.StatusUnauthorized, "invalid API key")
return
@ -467,6 +476,8 @@ func (g *Gateway) registerHandler(w http.ResponseWriter, r *http.Request) {
}
}
ctx := r.Context()
// Use internal context to bypass authentication for system operations
internalCtx := client.WithInternalAuth(ctx)
db := g.client.Database()
nsID, err := g.resolveNamespaceID(ctx, ns)
if err != nil {
@ -475,7 +486,7 @@ func (g *Gateway) registerHandler(w http.ResponseWriter, r *http.Request) {
}
// Validate nonce
q := "SELECT id FROM nonces WHERE namespace_id = ? AND wallet = ? AND nonce = ? AND used_at IS NULL AND (expires_at IS NULL OR expires_at > datetime('now')) LIMIT 1"
nres, err := db.Query(ctx, q, nsID, req.Wallet, req.Nonce)
nres, err := db.Query(internalCtx, q, nsID, req.Wallet, req.Nonce)
if err != nil || nres == nil || nres.Count == 0 || len(nres.Rows) == 0 || len(nres.Rows[0]) == 0 {
writeError(w, http.StatusBadRequest, "invalid or expired nonce")
return
@ -515,7 +526,7 @@ func (g *Gateway) registerHandler(w http.ResponseWriter, r *http.Request) {
}
// Mark nonce used now (after successful verification)
if _, err := db.Query(ctx, "UPDATE nonces SET used_at = datetime('now') WHERE id = ?", nonceID); err != nil {
if _, err := db.Query(internalCtx, "UPDATE nonces SET used_at = datetime('now') WHERE id = ?", nonceID); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
@ -533,13 +544,13 @@ func (g *Gateway) registerHandler(w http.ResponseWriter, r *http.Request) {
appID := "app_" + base64.RawURLEncoding.EncodeToString(buf)
// Persist app
if _, err := db.Query(ctx, "INSERT INTO apps(namespace_id, app_id, name, public_key) VALUES (?, ?, ?, ?)", nsID, appID, req.Name, pubHex); err != nil {
if _, err := db.Query(internalCtx, "INSERT INTO apps(namespace_id, app_id, name, public_key) VALUES (?, ?, ?, ?)", nsID, appID, req.Name, pubHex); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
// Record namespace ownership by wallet (best-effort)
_, _ = db.Query(ctx, "INSERT OR IGNORE INTO namespace_ownership(namespace_id, owner_type, owner_id) VALUES (?, ?, ?)", nsID, "wallet", req.Wallet)
_, _ = db.Query(internalCtx, "INSERT OR IGNORE INTO namespace_ownership(namespace_id, owner_type, owner_id) VALUES (?, ?, ?)", nsID, "wallet", req.Wallet)
writeJSON(w, http.StatusCreated, map[string]any{
"client_id": appID,
@ -583,6 +594,8 @@ func (g *Gateway) refreshHandler(w http.ResponseWriter, r *http.Request) {
}
}
ctx := r.Context()
// Use internal context to bypass authentication for system operations
internalCtx := client.WithInternalAuth(ctx)
db := g.client.Database()
nsID, err := g.resolveNamespaceID(ctx, ns)
if err != nil {
@ -590,7 +603,7 @@ func (g *Gateway) refreshHandler(w http.ResponseWriter, r *http.Request) {
return
}
q := "SELECT subject FROM refresh_tokens WHERE namespace_id = ? AND token = ? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > datetime('now')) LIMIT 1"
rres, err := db.Query(ctx, q, nsID, req.RefreshToken)
rres, err := db.Query(internalCtx, q, nsID, req.RefreshToken)
if err != nil || rres == nil || rres.Count == 0 {
writeError(w, http.StatusUnauthorized, "invalid or expired refresh token")
return
@ -972,6 +985,8 @@ func (g *Gateway) logoutHandler(w http.ResponseWriter, r *http.Request) {
}
}
ctx := r.Context()
// Use internal context to bypass authentication for system operations
internalCtx := client.WithInternalAuth(ctx)
db := g.client.Database()
nsID, err := g.resolveNamespaceID(ctx, ns)
if err != nil {
@ -981,7 +996,7 @@ func (g *Gateway) logoutHandler(w http.ResponseWriter, r *http.Request) {
if strings.TrimSpace(req.RefreshToken) != "" {
// Revoke specific token
if _, err := db.Query(ctx, "UPDATE refresh_tokens SET revoked_at = datetime('now') WHERE namespace_id = ? AND token = ? AND revoked_at IS NULL", nsID, req.RefreshToken); err != nil {
if _, err := db.Query(internalCtx, "UPDATE refresh_tokens SET revoked_at = datetime('now') WHERE namespace_id = ? AND token = ? AND revoked_at IS NULL", nsID, req.RefreshToken); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
@ -1001,7 +1016,7 @@ func (g *Gateway) logoutHandler(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusUnauthorized, "jwt required for all=true")
return
}
if _, err := db.Query(ctx, "UPDATE refresh_tokens SET revoked_at = datetime('now') WHERE namespace_id = ? AND subject = ? AND revoked_at IS NULL", nsID, subject); err != nil {
if _, err := db.Query(internalCtx, "UPDATE refresh_tokens SET revoked_at = datetime('now') WHERE namespace_id = ? AND subject = ? AND revoked_at IS NULL", nsID, subject); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}

View File

@ -2,14 +2,18 @@ package gateway
import (
"context"
"git.debros.io/DeBros/network/pkg/client"
)
func (g *Gateway) resolveNamespaceID(ctx context.Context, ns string) (interface{}, error) {
// Use internal context to bypass authentication for system operations
internalCtx := client.WithInternalAuth(ctx)
db := g.client.Database()
if _, err := db.Query(ctx, "INSERT OR IGNORE INTO namespaces(name) VALUES (?)", ns); err != nil {
if _, err := db.Query(internalCtx, "INSERT OR IGNORE INTO namespaces(name) VALUES (?)", ns); err != nil {
return nil, err
}
res, err := db.Query(ctx, "SELECT id FROM namespaces WHERE name = ? LIMIT 1", ns)
res, err := db.Query(internalCtx, "SELECT id FROM namespaces WHERE name = ? LIMIT 1", ns)
if err != nil || res == nil || res.Count == 0 || len(res.Rows) == 0 || len(res.Rows[0]) == 0 {
return nil, err
}