Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 70 additions & 78 deletions cmd/auth/login/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@ import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"
"time"

"github.com/charmbracelet/huh"
"github.com/fosrl/cli/internal/api"
"github.com/fosrl/cli/internal/secrets"
"github.com/fosrl/cli/internal/config"
"github.com/fosrl/cli/internal/logger"
"github.com/fosrl/cli/internal/utils"
"github.com/pkg/browser"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

type HostingOption string
Expand Down Expand Up @@ -72,8 +71,8 @@ func loginWithWeb(hostname string) (string, error) {
loginURL := fmt.Sprintf("%s?code=%s", baseLoginURL, code)

// Display code and instructions (similar to GH CLI format)
utils.Info("First copy your one-time code: %s", code)
utils.Info("Press Enter to open %s in your browser...", baseLoginURL)
logger.Info("First copy your one-time code: %s", code)
logger.Info("Press Enter to open %s in your browser...", baseLoginURL)

// Wait for Enter in a goroutine (non-blocking) and open browser when pressed
go func() {
Expand All @@ -83,8 +82,8 @@ func loginWithWeb(hostname string) (string, error) {
// User pressed Enter, open browser
if err := browser.OpenURL(loginURL); err != nil {
// Don't fail if browser can't be opened, just warn
utils.Warning("Failed to open browser automatically")
utils.Info("Please manually visit: %s", baseLoginURL)
logger.Warning("Failed to open browser automatically")
logger.Info("Please manually visit: %s", baseLoginURL)
}
}
}()
Expand All @@ -97,42 +96,42 @@ func loginWithWeb(hostname string) (string, error) {
var token string

for {
//print
utils.Debug("Polling for device web auth verification...")
// print
logger.Debug("Polling for device web auth verification...")
// Check if code has expired
if time.Now().After(expiresAt) {
utils.Error("Device web auth code has expired")
logger.Error("Device web auth code has expired")
return "", fmt.Errorf("code expired. Please try again")
}

// Check if we've exceeded max polling duration
if time.Since(startTime) > maxPollDuration {
utils.Error("Polling timed out after %v", maxPollDuration)
logger.Error("Polling timed out after %v", maxPollDuration)
return "", fmt.Errorf("polling timeout. Please try again")
}

// Poll for verification status
pollResp, message, err := api.PollDeviceWebAuth(loginClient, code)
// print debug info
utils.Debug("Polling response: %+v, message: %s, err: %v", pollResp, message, err)
logger.Debug("Polling response: %+v, message: %s, err: %v", pollResp, message, err)
if err != nil {
utils.Error("Error polling device web auth: %v", err)
logger.Error("Error polling device web auth: %v", err)
return "", fmt.Errorf("failed to poll device web auth: %w", err)
}

// Check verification status
if pollResp.Verified {
token = pollResp.Token
if token == "" {
utils.Error("Verification succeeded but no token received")
logger.Error("Verification succeeded but no token received")
return "", fmt.Errorf("verification succeeded but no token received")
}
return token, nil
}

// Check for expired or not found messages
if message == "Code expired" || message == "Code not found" {
utils.Error("Device web auth code has expired or not found")
logger.Error("Device web auth code has expired or not found")
return "", fmt.Errorf("code expired or not found. Please try again")
}

Expand All @@ -147,17 +146,8 @@ var LoginCmd = &cobra.Command{
Long: "Interactive login to select your hosting option and configure access.",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
// Check if user is already logged in
if err := utils.EnsureLoggedIn(); err == nil {
// User is logged in, show error with account info
email := viper.GetString("email")
var accountInfo string
if email != "" {
accountInfo = fmt.Sprintf(" (%s)", email)
}
utils.Error("You are already logged in%s. Please logout first using 'pangolin logout'", accountInfo)
return
}
apiClient := api.FromContext(cmd.Context())
accountStore := config.AccountStoreFromContext(cmd.Context())

var hostingOption HostingOption
var hostname string
Expand All @@ -183,7 +173,7 @@ var LoginCmd = &cobra.Command{
)

if err := form.Run(); err != nil {
utils.Error("Error: %v", err)
logger.Error("Error: %v", err)
return
}

Expand All @@ -199,7 +189,7 @@ var LoginCmd = &cobra.Command{
)

if err := hostnameForm.Run(); err != nil {
utils.Error("Error: %v", err)
logger.Error("Error: %v", err)
return
}
} else {
Expand All @@ -216,79 +206,81 @@ var LoginCmd = &cobra.Command{
hostname = "https://" + hostname
}

// Store hostname in viper config (with protocol)
viper.Set("hostname", hostname)

// Ensure config type is set and file path is correct
if viper.ConfigFileUsed() == "" {
// Config file doesn't exist yet, set the full path
// Get .pangolin directory and ensure it exists
pangolinDir, err := utils.GetPangolinDir()
if err == nil {
viper.SetConfigFile(filepath.Join(pangolinDir, "pangolin.json"))
viper.SetConfigType("json")
}
}

if err := viper.WriteConfig(); err != nil {
// If config file doesn't exist, create it
if err := viper.SafeWriteConfig(); err != nil {
utils.Warning("Failed to save hostname to config: %v", err)
}
}

// Perform web login
sessionToken, err := loginWithWeb(hostname)

if err != nil {
utils.Error("%v", err)
logger.Error("%v", err)
return
}

if sessionToken == "" {
utils.Error("Login appeared successful but no session token was received.")
return
}

// Save session token to config
if err := secrets.SaveSessionToken(sessionToken); err != nil {
utils.Error("Failed to save session token: %v", err)
logger.Error("Login appeared successful but no session token was received.")
return
}

// Update the global API client (always initialized)
// Update base URL and token (hostname already includes protocol)
apiBaseURL := hostname + "/api/v1"
api.GlobalClient.SetBaseURL(apiBaseURL)
api.GlobalClient.SetToken(sessionToken)
apiClient.SetBaseURL(apiBaseURL)
apiClient.SetToken(sessionToken)

utils.Success("Device authorized")
logger.Success("Device authorized")
fmt.Println()

// Get user information
var user *api.User
user, err = api.GlobalClient.GetUser()
user, err = apiClient.GetUser()
if err != nil {
utils.Warning("Failed to get user information: %v", err)
} else {
// Store userId and email in viper config
viper.Set("userId", user.UserID)
viper.Set("email", user.Email)
if err := viper.WriteConfig(); err != nil {
utils.Warning("Failed to save user information to config: %v", err)
}
logger.Error("Failed to get user information: %v", err)
return // FIXME: handle errors properly with exit codes!
}

// Ensure OLM credentials exist and are valid
userID := user.UserID
if err := utils.EnsureOlmCredentials(userID); err != nil {
utils.Warning("Failed to ensure OLM credentials: %v", err)
}
if _, exists := accountStore.Accounts[user.UserID]; exists {
logger.Warning("Already logged in as this user; no action needed")
return
}

// Ensure OLM credentials exist and are valid
userID := user.UserID

orgID, err := utils.SelectOrgForm(apiClient, userID)
if err != nil {
logger.Error("Failed to select organization: %v", err)
return
}

newOlmCreds, err := apiClient.CreateOlm(userID, utils.GetDeviceName())
if err != nil {
logger.Error("Failed to obtain olm credentials: %v", err)
return
}

newAccount := config.Account{
UserID: userID,
Host: hostname,
Email: user.Email,
SessionToken: sessionToken,
OrgID: orgID,
OlmCredentials: &config.OlmCredentials{
ID: newOlmCreds.OlmID,
Secret: newOlmCreds.Secret,
},
}

accountStore.Accounts[user.UserID] = newAccount
accountStore.ActiveUserID = userID

err = accountStore.Save()
if err != nil {
logger.Error("Failed to save account store: %s", err)
logger.Warning("You may not be able to login properly until this is saved.")
return
}

// List and select organization
if user != nil {
if _, err := utils.SelectOrg(user.UserID); err != nil {
utils.Warning("%v", err)
if _, err := utils.SelectOrgForm(apiClient, user.UserID); err != nil {
logger.Warning("%v", err)
}
}

Expand All @@ -299,7 +291,7 @@ var LoginCmd = &cobra.Command{
displayName = *user.Username
}
if displayName != "" {
utils.Success("Logged in as %s", displayName)
logger.Success("Logged in as %s", displayName)
}
}
},
Expand Down
Loading