feat(cli): add login command and refactor shared helpers
This commit is contained in:
parent
0dafeaa70d
commit
4d35e197f0
8 changed files with 296 additions and 49 deletions
187
internal/cmd/login.go
Normal file
187
internal/cmd/login.go
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/fatih/color"
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/torrentclaw/unarr/internal/agent"
|
||||
"github.com/torrentclaw/unarr/internal/config"
|
||||
)
|
||||
|
||||
func newLoginCmd() *cobra.Command {
|
||||
var apiURL string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "login",
|
||||
Aliases: []string{"auth"},
|
||||
Short: "Authenticate with your torrentclaw account",
|
||||
Long: `Log in to your torrentclaw account by opening the browser or pasting
|
||||
your API key manually. Use this when your API key has expired, been
|
||||
revoked, or you want to switch to a different account.
|
||||
|
||||
Unlike 'unarr init', this command only updates your authentication
|
||||
credentials — it does not modify your download directory, daemon
|
||||
settings, or other configuration.`,
|
||||
Example: ` unarr login
|
||||
unarr login --api-url https://custom.server.com`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runLogin(apiURL)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&apiURL, "api-url", "", "API URL override (default: https://torrentclaw.com)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runLogin(apiURLOverride string) error {
|
||||
if !isTerminal() {
|
||||
return fmt.Errorf("interactive mode requires a terminal (use UNARR_API_KEY env var instead)")
|
||||
}
|
||||
|
||||
bold := color.New(color.Bold)
|
||||
green := color.New(color.FgGreen)
|
||||
dim := color.New(color.FgHiBlack)
|
||||
|
||||
fmt.Println()
|
||||
bold.Println(" unarr login")
|
||||
fmt.Println()
|
||||
|
||||
cfg := loadConfig()
|
||||
|
||||
// Determine API URL
|
||||
apiURL := cfg.Auth.APIURL
|
||||
if apiURLOverride != "" {
|
||||
apiURL = apiURLOverride
|
||||
}
|
||||
if apiURL == "" {
|
||||
apiURL = "https://torrentclaw.com"
|
||||
}
|
||||
|
||||
// ── Authenticate ────────────────────────────────────────────────
|
||||
|
||||
var apiKey string
|
||||
|
||||
// Try browser-based auth first
|
||||
fmt.Println(" Opening browser to connect your account...")
|
||||
fmt.Println()
|
||||
|
||||
browserKey, browserErr := browserAuth(apiURL)
|
||||
if browserErr == nil && strings.HasPrefix(browserKey, "tc_") {
|
||||
apiKey = browserKey
|
||||
green.Println(" ✓ Connected via browser")
|
||||
fmt.Println()
|
||||
} else {
|
||||
// Fallback to manual API key entry
|
||||
if browserErr != nil {
|
||||
dim.Printf(" Could not connect automatically: %s\n", browserErr)
|
||||
}
|
||||
fmt.Println(" Paste your API key instead:")
|
||||
dim.Printf(" (get it from %s/profile?tab=apikey)\n", apiURL)
|
||||
fmt.Println()
|
||||
|
||||
err := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title("API Key").
|
||||
Placeholder("tc_...").
|
||||
Value(&apiKey).
|
||||
Validate(func(s string) error {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return fmt.Errorf("API key is required")
|
||||
}
|
||||
if !strings.HasPrefix(s, "tc_") {
|
||||
return fmt.Errorf("API key should start with tc_")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
),
|
||||
).Run()
|
||||
if err != nil {
|
||||
if errors.Is(err, huh.ErrUserAborted) {
|
||||
fmt.Println("\n Login cancelled.")
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
apiKey = strings.TrimSpace(apiKey)
|
||||
}
|
||||
|
||||
// ── Validate API key ────────────────────────────────────────────
|
||||
|
||||
fmt.Print(" Verifying API key... ")
|
||||
|
||||
agentID := cfg.Agent.ID
|
||||
if agentID == "" {
|
||||
agentID = uuid.New().String()
|
||||
}
|
||||
|
||||
hostname, _ := os.Hostname()
|
||||
agentName := cfg.Agent.Name
|
||||
if agentName == "" {
|
||||
agentName = hostname
|
||||
}
|
||||
|
||||
ac := agent.NewClient(apiURL, apiKey, "unarr/"+Version)
|
||||
resp, err := ac.Register(context.Background(), agent.RegisterRequest{
|
||||
AgentID: agentID,
|
||||
Name: agentName,
|
||||
OS: runtime.GOOS,
|
||||
Arch: runtime.GOARCH,
|
||||
Version: Version,
|
||||
DownloadDir: cfg.Download.Dir,
|
||||
})
|
||||
if err != nil {
|
||||
color.Red("FAILED")
|
||||
fmt.Println()
|
||||
return fmt.Errorf("API key validation failed: %w", err)
|
||||
}
|
||||
|
||||
green.Println("OK")
|
||||
fmt.Printf(" Connected as %s (%s) [%s]\n", resp.User.Name, resp.User.Email, strings.ToUpper(resp.User.Plan))
|
||||
fmt.Println()
|
||||
|
||||
// ── Save config (auth fields only) ──────────────────────────────
|
||||
|
||||
cfg.Auth.APIKey = apiKey
|
||||
cfg.Auth.APIURL = apiURL
|
||||
cfg.Agent.ID = agentID
|
||||
cfg.Agent.Name = agentName
|
||||
|
||||
configPath := config.FilePath()
|
||||
if cfgFile != "" {
|
||||
configPath = cfgFile
|
||||
}
|
||||
|
||||
if err := config.Save(cfg, configPath); err != nil {
|
||||
return fmt.Errorf("save config: %w", err)
|
||||
}
|
||||
appCfg = cfg
|
||||
|
||||
fmt.Println()
|
||||
green.Println(" ✓ Credentials saved!")
|
||||
fmt.Printf(" Config: %s\n", configPath)
|
||||
fmt.Println()
|
||||
|
||||
// Features summary
|
||||
if line := formatFeatures(resp.Features); line != "" {
|
||||
color.New(color.FgCyan).Printf(" Available: %s\n", line)
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
if cfg.Download.Dir == "" {
|
||||
fmt.Println(" Run " + bold.Sprint("unarr init") + " to complete the setup (download directory, daemon).")
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue