network/pkg/auth/simple_auth.go

314 lines
8.8 KiB
Go
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package auth
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"github.com/DeBrosOfficial/network/pkg/tlsutil"
)
// PerformSimpleAuthentication performs a simple authentication flow where the user
// provides a wallet address and receives an API key without signature verification
func PerformSimpleAuthentication(gatewayURL string) (*Credentials, error) {
reader := bufio.NewReader(os.Stdin)
fmt.Println("\n🔐 Simple Wallet Authentication")
fmt.Println("================================")
// Read wallet address
fmt.Print("Enter your wallet address (0x...): ")
walletInput, err := reader.ReadString('\n')
if err != nil {
return nil, fmt.Errorf("failed to read wallet address: %w", err)
}
wallet := strings.TrimSpace(walletInput)
if wallet == "" {
return nil, fmt.Errorf("wallet address cannot be empty")
}
// Validate wallet format (basic check)
if !strings.HasPrefix(wallet, "0x") && !strings.HasPrefix(wallet, "0X") {
wallet = "0x" + wallet
}
if !ValidateWalletAddress(wallet) {
return nil, fmt.Errorf("invalid wallet address format")
}
// Read namespace (required)
var namespace string
for {
fmt.Print("Enter namespace (required): ")
nsInput, err := reader.ReadString('\n')
if err != nil {
return nil, fmt.Errorf("failed to read namespace: %w", err)
}
namespace = strings.TrimSpace(nsInput)
if namespace != "" {
break
}
fmt.Println("⚠️ Namespace cannot be empty. Please enter a namespace.")
}
fmt.Printf("\n✅ Wallet: %s\n", wallet)
fmt.Printf("✅ Namespace: %s\n", namespace)
fmt.Println("⏳ Requesting API key from gateway...")
// Request API key from gateway
apiKey, err := requestAPIKeyFromGateway(gatewayURL, wallet, namespace)
if err != nil {
return nil, fmt.Errorf("failed to request API key: %w", err)
}
// Create credentials
creds := &Credentials{
APIKey: apiKey,
Namespace: namespace,
UserID: wallet,
Wallet: wallet,
IssuedAt: time.Now(),
}
fmt.Printf("\n🎉 Authentication successful!\n")
fmt.Printf("📝 API Key: %s\n", creds.APIKey)
return creds, nil
}
// requestAPIKeyFromGateway calls the gateway's simple-key endpoint to generate an API key
// For non-default namespaces, this may trigger cluster provisioning and require polling
func requestAPIKeyFromGateway(gatewayURL, wallet, namespace string) (string, error) {
reqBody := map[string]string{
"wallet": wallet,
"namespace": namespace,
}
payload, err := json.Marshal(reqBody)
if err != nil {
return "", fmt.Errorf("failed to marshal request: %w", err)
}
endpoint := gatewayURL + "/v1/auth/simple-key"
// Extract domain from URL for TLS configuration
// This uses tlsutil which handles Let's Encrypt staging certificates for *.orama.network
domain := extractDomainFromURL(gatewayURL)
client := tlsutil.NewHTTPClientForDomain(30*time.Second, domain)
resp, err := client.Post(endpoint, "application/json", bytes.NewReader(payload))
if err != nil {
return "", fmt.Errorf("failed to call gateway: %w", err)
}
defer resp.Body.Close()
// Handle 202 Accepted - namespace cluster is being provisioned
if resp.StatusCode == http.StatusAccepted {
return handleProvisioningResponse(gatewayURL, client, resp, wallet, namespace)
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("gateway returned status %d: %s", resp.StatusCode, string(body))
}
var respBody map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil {
return "", fmt.Errorf("failed to decode response: %w", err)
}
apiKey, ok := respBody["api_key"].(string)
if !ok || apiKey == "" {
return "", fmt.Errorf("no api_key in response")
}
return apiKey, nil
}
// handleProvisioningResponse handles 202 Accepted responses when namespace cluster provisioning is needed
func handleProvisioningResponse(gatewayURL string, client *http.Client, resp *http.Response, wallet, namespace string) (string, error) {
var provResp map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&provResp); err != nil {
return "", fmt.Errorf("failed to decode provisioning response: %w", err)
}
status, _ := provResp["status"].(string)
pollURL, _ := provResp["poll_url"].(string)
clusterID, _ := provResp["cluster_id"].(string)
message, _ := provResp["message"].(string)
if status != "provisioning" {
return "", fmt.Errorf("unexpected status: %s", status)
}
fmt.Printf("\n🏗 Provisioning namespace cluster...\n")
if message != "" {
fmt.Printf(" %s\n", message)
}
if clusterID != "" {
fmt.Printf(" Cluster ID: %s\n", clusterID)
}
fmt.Println()
// Poll until cluster is ready
if err := pollProvisioningStatus(gatewayURL, client, pollURL); err != nil {
return "", err
}
// Cluster is ready, retry the API key request
fmt.Println("\n✅ Namespace cluster ready!")
fmt.Println("⏳ Retrieving API key...")
return retryAPIKeyRequest(gatewayURL, client, wallet, namespace)
}
// pollProvisioningStatus polls the status endpoint until the cluster is ready
func pollProvisioningStatus(gatewayURL string, client *http.Client, pollURL string) error {
// Build full poll URL if it's a relative path
if strings.HasPrefix(pollURL, "/") {
pollURL = gatewayURL + pollURL
}
maxAttempts := 120 // 10 minutes (5 seconds per poll)
pollInterval := 5 * time.Second
spinnerChars := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
spinnerIdx := 0
for i := 0; i < maxAttempts; i++ {
// Show progress spinner
fmt.Printf("\r%s Waiting for cluster... ", spinnerChars[spinnerIdx%len(spinnerChars)])
spinnerIdx++
resp, err := client.Get(pollURL)
if err != nil {
time.Sleep(pollInterval)
continue
}
var statusResp map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&statusResp); err != nil {
resp.Body.Close()
time.Sleep(pollInterval)
continue
}
resp.Body.Close()
status, _ := statusResp["status"].(string)
switch status {
case "ready":
fmt.Printf("\r✅ Cluster ready! \n")
return nil
case "failed":
errMsg, _ := statusResp["error"].(string)
fmt.Printf("\r❌ Provisioning failed \n")
return fmt.Errorf("cluster provisioning failed: %s", errMsg)
case "provisioning":
// Show progress details
rqliteReady, _ := statusResp["rqlite_ready"].(bool)
olricReady, _ := statusResp["olric_ready"].(bool)
gatewayReady, _ := statusResp["gateway_ready"].(bool)
dnsReady, _ := statusResp["dns_ready"].(bool)
progressStr := ""
if rqliteReady {
progressStr += "RQLite✓ "
}
if olricReady {
progressStr += "Olric✓ "
}
if gatewayReady {
progressStr += "Gateway✓ "
}
if dnsReady {
progressStr += "DNS✓"
}
if progressStr != "" {
fmt.Printf("\r%s Provisioning... [%s]", spinnerChars[spinnerIdx%len(spinnerChars)], progressStr)
}
default:
// Unknown status, continue polling
}
time.Sleep(pollInterval)
}
fmt.Printf("\r⚠ Timeout waiting for cluster \n")
return fmt.Errorf("timeout waiting for namespace cluster provisioning")
}
// retryAPIKeyRequest retries the API key request after cluster provisioning
func retryAPIKeyRequest(gatewayURL string, client *http.Client, wallet, namespace string) (string, error) {
reqBody := map[string]string{
"wallet": wallet,
"namespace": namespace,
}
payload, err := json.Marshal(reqBody)
if err != nil {
return "", fmt.Errorf("failed to marshal request: %w", err)
}
endpoint := gatewayURL + "/v1/auth/simple-key"
resp, err := client.Post(endpoint, "application/json", bytes.NewReader(payload))
if err != nil {
return "", fmt.Errorf("failed to call gateway: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusAccepted {
// Still provisioning? This shouldn't happen but handle gracefully
return "", fmt.Errorf("cluster still provisioning, please try again")
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("gateway returned status %d: %s", resp.StatusCode, string(body))
}
var respBody map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil {
return "", fmt.Errorf("failed to decode response: %w", err)
}
apiKey, ok := respBody["api_key"].(string)
if !ok || apiKey == "" {
return "", fmt.Errorf("no api_key in response")
}
return apiKey, nil
}
// extractDomainFromURL extracts the domain from a URL
// Removes protocol (https://, http://), path, and port components
func extractDomainFromURL(url string) string {
// Remove protocol prefixes
url = strings.TrimPrefix(url, "https://")
url = strings.TrimPrefix(url, "http://")
// Remove path component
if idx := strings.Index(url, "/"); idx != -1 {
url = url[:idx]
}
// Remove port component
if idx := strings.Index(url, ":"); idx != -1 {
url = url[:idx]
}
return url
}