mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-17 18:36:57 +00:00
215 lines
6.2 KiB
Go
215 lines
6.2 KiB
Go
package auth
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/DeBrosOfficial/network/pkg/tlsutil"
|
|
qrterminal "github.com/mdp/qrterminal/v3"
|
|
)
|
|
|
|
// defaultPhantomAuthURL is the default Phantom auth React app URL (deployed on Orama devnet).
|
|
// Override with ORAMA_PHANTOM_AUTH_URL environment variable.
|
|
const defaultPhantomAuthURL = "https://phantom-auth-y0w9aa.orama-devnet.network"
|
|
|
|
// phantomAuthURL returns the Phantom auth URL, preferring the environment variable.
|
|
func phantomAuthURL() string {
|
|
if u := os.Getenv("ORAMA_PHANTOM_AUTH_URL"); u != "" {
|
|
return strings.TrimRight(u, "/")
|
|
}
|
|
return defaultPhantomAuthURL
|
|
}
|
|
|
|
// PhantomSession represents a phantom auth session from the gateway.
|
|
type PhantomSession struct {
|
|
SessionID string `json:"session_id"`
|
|
ExpiresAt string `json:"expires_at"`
|
|
}
|
|
|
|
// PhantomSessionStatus represents the polled status of a phantom auth session.
|
|
type PhantomSessionStatus struct {
|
|
SessionID string `json:"session_id"`
|
|
Status string `json:"status"`
|
|
Wallet string `json:"wallet"`
|
|
APIKey string `json:"api_key"`
|
|
Namespace string `json:"namespace"`
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
// PerformPhantomAuthentication runs the Phantom Solana auth flow:
|
|
// 1. Prompt for namespace
|
|
// 2. Create session via gateway
|
|
// 3. Display QR code in terminal
|
|
// 4. Poll for completion
|
|
// 5. Return credentials
|
|
func PerformPhantomAuthentication(gatewayURL, namespace string) (*Credentials, error) {
|
|
reader := bufio.NewReader(os.Stdin)
|
|
|
|
fmt.Println("\n🟣 Phantom Wallet Authentication (Solana)")
|
|
fmt.Println("==========================================")
|
|
fmt.Println("Requires an NFT from the authorized collection.")
|
|
|
|
// Prompt for namespace if empty
|
|
if namespace == "" {
|
|
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.")
|
|
}
|
|
}
|
|
|
|
domain := extractDomainFromURL(gatewayURL)
|
|
client := tlsutil.NewHTTPClientForDomain(30*time.Second, domain)
|
|
|
|
// 1. Create phantom session
|
|
fmt.Println("\nCreating authentication session...")
|
|
session, err := createPhantomSession(client, gatewayURL, namespace)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create session: %w", err)
|
|
}
|
|
|
|
// 2. Build auth URL and display QR code
|
|
authURL := fmt.Sprintf("%s/?session=%s&gateway=%s&namespace=%s",
|
|
phantomAuthURL(), session.SessionID, url.QueryEscape(gatewayURL), url.QueryEscape(namespace))
|
|
|
|
fmt.Println("\nScan this QR code with your phone to authenticate:")
|
|
fmt.Println()
|
|
qrterminal.GenerateWithConfig(authURL, qrterminal.Config{
|
|
Level: qrterminal.M,
|
|
Writer: os.Stdout,
|
|
BlackChar: qrterminal.BLACK,
|
|
WhiteChar: qrterminal.WHITE,
|
|
QuietZone: 1,
|
|
})
|
|
fmt.Println()
|
|
fmt.Printf("Or open this URL on your phone:\n%s\n\n", authURL)
|
|
fmt.Println("Waiting for authentication... (timeout: 5 minutes)")
|
|
|
|
// 3. Poll for completion
|
|
creds, err := pollPhantomSession(client, gatewayURL, session.SessionID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Set namespace and build namespace URL
|
|
creds.Namespace = namespace
|
|
if domain := extractDomainFromURL(gatewayURL); domain != "" {
|
|
if namespace == "default" {
|
|
creds.NamespaceURL = fmt.Sprintf("https://%s", domain)
|
|
} else {
|
|
creds.NamespaceURL = fmt.Sprintf("https://ns-%s.%s", namespace, domain)
|
|
}
|
|
}
|
|
|
|
fmt.Printf("\n🎉 Authentication successful!\n")
|
|
truncatedKey := creds.APIKey
|
|
if len(truncatedKey) > 8 {
|
|
truncatedKey = truncatedKey[:8] + "..."
|
|
}
|
|
fmt.Printf("📝 API Key: %s\n", truncatedKey)
|
|
|
|
return creds, nil
|
|
}
|
|
|
|
// createPhantomSession creates a new phantom auth session via the gateway.
|
|
func createPhantomSession(client *http.Client, gatewayURL, namespace string) (*PhantomSession, error) {
|
|
reqBody := map[string]string{
|
|
"namespace": namespace,
|
|
}
|
|
payload, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := client.Post(gatewayURL+"/v1/auth/phantom/session", "application/json", bytes.NewReader(payload))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to call gateway: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("gateway returned status %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var session PhantomSession
|
|
if err := json.NewDecoder(resp.Body).Decode(&session); err != nil {
|
|
return nil, fmt.Errorf("failed to decode response: %w", err)
|
|
}
|
|
|
|
return &session, nil
|
|
}
|
|
|
|
// pollPhantomSession polls the gateway for session completion.
|
|
func pollPhantomSession(client *http.Client, gatewayURL, sessionID string) (*Credentials, error) {
|
|
pollInterval := 2 * time.Second
|
|
maxDuration := 5 * time.Minute
|
|
deadline := time.Now().Add(maxDuration)
|
|
|
|
spinnerChars := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
|
spinnerIdx := 0
|
|
|
|
for time.Now().Before(deadline) {
|
|
resp, err := client.Get(gatewayURL + "/v1/auth/phantom/session/" + sessionID)
|
|
if err != nil {
|
|
time.Sleep(pollInterval)
|
|
continue
|
|
}
|
|
|
|
var status PhantomSessionStatus
|
|
if err := json.NewDecoder(resp.Body).Decode(&status); err != nil {
|
|
resp.Body.Close()
|
|
time.Sleep(pollInterval)
|
|
continue
|
|
}
|
|
resp.Body.Close()
|
|
|
|
switch status.Status {
|
|
case "completed":
|
|
fmt.Printf("\r✅ Authenticated! \n")
|
|
return &Credentials{
|
|
APIKey: status.APIKey,
|
|
Wallet: status.Wallet,
|
|
UserID: status.Wallet,
|
|
IssuedAt: time.Now(),
|
|
}, nil
|
|
|
|
case "failed":
|
|
fmt.Printf("\r❌ Authentication failed \n")
|
|
errMsg := status.Error
|
|
if errMsg == "" {
|
|
errMsg = "unknown error"
|
|
}
|
|
return nil, fmt.Errorf("authentication failed: %s", errMsg)
|
|
|
|
case "expired":
|
|
fmt.Printf("\r⏰ Session expired \n")
|
|
return nil, fmt.Errorf("authentication session expired")
|
|
|
|
case "pending":
|
|
fmt.Printf("\r%s Waiting for phone authentication... ", spinnerChars[spinnerIdx%len(spinnerChars)])
|
|
spinnerIdx++
|
|
}
|
|
|
|
time.Sleep(pollInterval)
|
|
}
|
|
|
|
fmt.Printf("\r⏰ Timeout \n")
|
|
return nil, fmt.Errorf("authentication timed out after 5 minutes")
|
|
}
|