unarr/internal/cmd/login.go

187 lines
4.7 KiB
Go

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
}