diff --git a/CHANGELOG.md b/CHANGELOG.md index 29758a2..33d119d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,14 +8,21 @@ The format is based on [Keep a Changelog][keepachangelog] and adheres to [Semant ### Added +- Added validation for yaml files +- Added authenticaiton command on cli + ### Changed +- Updated readme + ### Deprecated ### Removed ### Fixed +- Regular nodes rqlite not starting + ## [0.51.2] - 2025-09-26 ### Added diff --git a/Makefile b/Makefile index a77870b..4e611b4 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ test-e2e: .PHONY: build clean test run-node run-node2 run-node3 run-example deps tidy fmt vet lint clear-ports -VERSION := 0.51.3-beta +VERSION := 0.51.4-beta COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ) LDFLAGS := -X 'main.version=$(VERSION)' -X 'main.commit=$(COMMIT)' -X 'main.date=$(DATE)' diff --git a/cmd/cli/main.go b/cmd/cli/main.go index e115113..f9acc47 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -76,6 +76,8 @@ func main() { handleConnect(args[0]) case "peer-id": handlePeerID() + case "auth": + handleAuth(args) case "help", "--help", "-h": showHelp() @@ -289,6 +291,145 @@ func handlePubSub(args []string) { } } +func handleAuth(args []string) { + if len(args) == 0 { + showAuthHelp() + return + } + + subcommand := args[0] + switch subcommand { + case "login": + handleAuthLogin() + case "logout": + handleAuthLogout() + case "whoami": + handleAuthWhoami() + case "status": + handleAuthStatus() + default: + fmt.Fprintf(os.Stderr, "Unknown auth command: %s\n", subcommand) + showAuthHelp() + os.Exit(1) + } +} + +func handleAuthLogin() { + gatewayURL := auth.GetDefaultGatewayURL() + fmt.Printf("🔐 Authenticating with gateway at: %s\n", gatewayURL) + + // Use the wallet authentication flow + creds, err := auth.PerformWalletAuthentication(gatewayURL) + if err != nil { + fmt.Fprintf(os.Stderr, "❌ Authentication failed: %v\n", err) + os.Exit(1) + } + + // Save credentials to file + if err := auth.SaveCredentialsForDefaultGateway(creds); err != nil { + fmt.Fprintf(os.Stderr, "❌ Failed to save credentials: %v\n", err) + os.Exit(1) + } + + credsPath, _ := auth.GetCredentialsPath() + fmt.Printf("✅ Authentication successful!\n") + fmt.Printf("📁 Credentials saved to: %s\n", credsPath) + fmt.Printf("🎯 Wallet: %s\n", creds.Wallet) + fmt.Printf("🏢 Namespace: %s\n", creds.Namespace) +} + +func handleAuthLogout() { + if err := auth.ClearAllCredentials(); err != nil { + fmt.Fprintf(os.Stderr, "❌ Failed to clear credentials: %v\n", err) + os.Exit(1) + } + fmt.Println("✅ Logged out successfully - all credentials have been cleared") +} + +func handleAuthWhoami() { + store, err := auth.LoadCredentials() + if err != nil { + fmt.Fprintf(os.Stderr, "❌ Failed to load credentials: %v\n", err) + os.Exit(1) + } + + gatewayURL := auth.GetDefaultGatewayURL() + creds, exists := store.GetCredentialsForGateway(gatewayURL) + + if !exists || !creds.IsValid() { + fmt.Println("❌ Not authenticated - run 'network-cli auth login' to authenticate") + os.Exit(1) + } + + fmt.Println("✅ Authenticated") + fmt.Printf(" Wallet: %s\n", creds.Wallet) + fmt.Printf(" Namespace: %s\n", creds.Namespace) + fmt.Printf(" Issued At: %s\n", creds.IssuedAt.Format("2006-01-02 15:04:05")) + if !creds.ExpiresAt.IsZero() { + fmt.Printf(" Expires At: %s\n", creds.ExpiresAt.Format("2006-01-02 15:04:05")) + } + if !creds.LastUsedAt.IsZero() { + fmt.Printf(" Last Used: %s\n", creds.LastUsedAt.Format("2006-01-02 15:04:05")) + } + if creds.Plan != "" { + fmt.Printf(" Plan: %s\n", creds.Plan) + } +} + +func handleAuthStatus() { + store, err := auth.LoadCredentials() + if err != nil { + fmt.Fprintf(os.Stderr, "❌ Failed to load credentials: %v\n", err) + os.Exit(1) + } + + gatewayURL := auth.GetDefaultGatewayURL() + creds, exists := store.GetCredentialsForGateway(gatewayURL) + + fmt.Println("🔐 Authentication Status") + fmt.Printf(" Gateway URL: %s\n", gatewayURL) + + if !exists || creds == nil { + fmt.Println(" Status: ❌ Not authenticated") + return + } + + if !creds.IsValid() { + fmt.Println(" Status: ⚠️ Credentials expired") + if !creds.ExpiresAt.IsZero() { + fmt.Printf(" Expired At: %s\n", creds.ExpiresAt.Format("2006-01-02 15:04:05")) + } + return + } + + fmt.Println(" Status: ✅ Authenticated") + fmt.Printf(" Wallet: %s\n", creds.Wallet) + fmt.Printf(" Namespace: %s\n", creds.Namespace) + if !creds.ExpiresAt.IsZero() { + fmt.Printf(" Expires: %s\n", creds.ExpiresAt.Format("2006-01-02 15:04:05")) + } + if !creds.LastUsedAt.IsZero() { + fmt.Printf(" Last Used: %s\n", creds.LastUsedAt.Format("2006-01-02 15:04:05")) + } +} + +func showAuthHelp() { + fmt.Printf("🔐 Authentication Commands\n\n") + fmt.Printf("Usage: network-cli auth \n\n") + fmt.Printf("Subcommands:\n") + fmt.Printf(" login - Authenticate with wallet\n") + fmt.Printf(" logout - Clear stored credentials\n") + fmt.Printf(" whoami - Show current authentication status\n") + fmt.Printf(" status - Show detailed authentication info\n\n") + fmt.Printf("Examples:\n") + fmt.Printf(" network-cli auth login\n") + fmt.Printf(" network-cli auth whoami\n") + fmt.Printf(" network-cli auth status\n") + fmt.Printf(" network-cli auth logout\n\n") + fmt.Printf("Environment Variables:\n") + fmt.Printf(" DEBROS_GATEWAY_URL - Gateway URL (default: http://localhost:6001)\n") +} + func ensureAuthenticated() *auth.Credentials { gatewayURL := auth.GetDefaultGatewayURL() @@ -440,6 +581,7 @@ func showHelp() { fmt.Printf("Usage: network-cli [args...]\n\n") fmt.Printf("🔐 Authentication: Commands requiring authentication will automatically prompt for wallet connection.\n\n") fmt.Printf("Commands:\n") + fmt.Printf(" auth 🔐 Authentication management (login, logout, whoami, status)\n") fmt.Printf(" health - Check network health\n") fmt.Printf(" peers - List connected peers\n") fmt.Printf(" status - Show network status\n") @@ -457,10 +599,13 @@ func showHelp() { fmt.Printf(" -t, --timeout - Operation timeout (default: 30s)\n") fmt.Printf(" --production - Connect to production bootstrap peers\n\n") fmt.Printf("Authentication:\n") + fmt.Printf(" Use 'network-cli auth login' to authenticate with your wallet\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 auth login\n") + fmt.Printf(" network-cli auth whoami\n") fmt.Printf(" network-cli health\n") fmt.Printf(" network-cli peer-id\n") fmt.Printf(" network-cli peer-id --format json\n") diff --git a/pkg/gateway/config_validate.go b/pkg/gateway/config_validate.go index ebb4686..a185107 100644 --- a/pkg/gateway/config_validate.go +++ b/pkg/gateway/config_validate.go @@ -8,7 +8,6 @@ import ( "strings" "github.com/multiformats/go-multiaddr" - manet "github.com/multiformats/go-multiaddr/net" ) // ValidateConfig performs comprehensive validation of gateway configuration. @@ -35,7 +34,7 @@ func (c *Config) ValidateConfig() []error { for i, peer := range c.BootstrapPeers { path := fmt.Sprintf("gateway.bootstrap_peers[%d]", i) - ma, err := multiaddr.NewMultiaddr(peer) + _, err := multiaddr.NewMultiaddr(peer) if err != nil { errs = append(errs, fmt.Errorf("%s: invalid multiaddr: %v; expected /ip{4,6}/.../tcp//p2p/", path, err)) continue @@ -46,16 +45,16 @@ func (c *Config) ValidateConfig() []error { errs = append(errs, fmt.Errorf("%s: missing /p2p/ component; expected /ip{4,6}/.../tcp//p2p/", path)) } - // Try to extract TCP addr to validate port - tcpAddr, err := manet.ToNetAddr(ma) - if err != nil { - errs = append(errs, fmt.Errorf("%s: cannot convert to network address: %v", path, err)) + // Extract TCP port by parsing the multiaddr string directly + tcpPortStr := extractTCPPort(peer) + if tcpPortStr == "" { + errs = append(errs, fmt.Errorf("%s: missing /tcp/ component; expected /ip{4,6}/.../tcp//p2p/", path)) continue } - tcpPort := tcpAddr.(*net.TCPAddr).Port - if tcpPort < 1 || tcpPort > 65535 { - errs = append(errs, fmt.Errorf("%s: invalid TCP port %d; port must be between 1 and 65535", path, tcpPort)) + tcpPort, err := strconv.Atoi(tcpPortStr) + if err != nil || tcpPort < 1 || tcpPort > 65535 { + errs = append(errs, fmt.Errorf("%s: invalid TCP port %s; port must be between 1 and 65535", path, tcpPortStr)) } if seenPeers[peer] { @@ -115,3 +114,24 @@ func validateRQLiteDSN(dsn string) error { return nil } + +// extractTCPPort extracts the TCP port from a multiaddr string. +// It assumes the multiaddr is in the format /ip{4,6}/.../tcp//p2p/. +func extractTCPPort(multiaddrStr string) string { + // Find the last /tcp/ component + lastTCPIndex := strings.LastIndex(multiaddrStr, "/tcp/") + if lastTCPIndex == -1 { + return "" + } + + // Extract the port part after /tcp/ + portPart := multiaddrStr[lastTCPIndex+len("/tcp/"):] + + // Find the first / component after the port part + firstSlashIndex := strings.Index(portPart, "/") + if firstSlashIndex == -1 { + return portPart + } + + return portPart[:firstSlashIndex] +}