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
20 changes: 11 additions & 9 deletions cmd/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ import (
"github.com/spf13/cobra"
)

var AuthCmd = &cobra.Command{
Use: "auth",
Short: "Authentication commands",
Long: "Manage authentication and sessions",
}
func AuthCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "auth",
Short: "Authentication commands",
Long: "Manage authentication and sessions",
}

cmd.AddCommand(login.LoginCmd())
cmd.AddCommand(logout.LogoutCmd())
cmd.AddCommand(status.StatusCmd())

func init() {
AuthCmd.AddCommand(login.LoginCmd)
AuthCmd.AddCommand(logout.LogoutCmd)
AuthCmd.AddCommand(status.StatusCmd)
return cmd
}
276 changes: 151 additions & 125 deletions cmd/auth/login/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package login

import (
"bufio"
"errors"
"fmt"
"os"
"strings"
Expand Down Expand Up @@ -140,159 +141,184 @@ func loginWithWeb(hostname string) (string, error) {
}
}

var LoginCmd = &cobra.Command{
Use: "login [hostname]",
Short: "Login to Pangolin",
Long: "Interactive login to select your hosting option and configure access.",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
apiClient := api.FromContext(cmd.Context())
accountStore := config.AccountStoreFromContext(cmd.Context())
type LoginCmdOpts struct {
Hostname string
}

func LoginCmd() *cobra.Command {
opts := LoginCmdOpts{}

cmd := &cobra.Command{
Use: "login [hostname]",
Short: "Login to Pangolin",
Long: "Interactive login to select your hosting option and configure access.",
Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.MaximumNArgs(1)(cmd, args); err != nil {
return err
}

if len(args) > 0 {
opts.Hostname = args[0]
}

return nil
},
Run: func(cmd *cobra.Command, args []string) {
if err := loginMain(cmd, &opts); err != nil {
os.Exit(1)
}
},
}

return cmd
}

func loginMain(cmd *cobra.Command, opts *LoginCmdOpts) error {
apiClient := api.FromContext(cmd.Context())
accountStore := config.AccountStoreFromContext(cmd.Context())

hostname := opts.Hostname

// If hostname was provided, skip hosting option selection
if hostname == "" {
var hostingOption HostingOption
var hostname string

// Check if hostname was provided as positional argument
if len(args) > 0 {
hostname = args[0]
// First question: select hosting option
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[HostingOption]().
Title("Select your hosting option").
Options(
huh.NewOption("Pangolin Cloud (app.pangolin.net)", HostingOptionCloud),
huh.NewOption("Self-hosted or Dedicated instance", HostingOptionSelfHosted),
).
Value(&hostingOption),
),
)

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

// If hostname was provided, skip hosting option selection
if hostname == "" {
// First question: select hosting option
form := huh.NewForm(
// If self-hosted, prompt for hostname
if hostingOption == HostingOptionSelfHosted {
hostnameForm := huh.NewForm(
huh.NewGroup(
huh.NewSelect[HostingOption]().
Title("Select your hosting option").
Options(
huh.NewOption("Pangolin Cloud (app.pangolin.net)", HostingOptionCloud),
huh.NewOption("Self-hosted or Dedicated instance", HostingOptionSelfHosted),
).
Value(&hostingOption),
huh.NewInput().
Title("Enter hostname URL").
Placeholder("https://your-instance.example.com").
Value(&hostname),
),
)

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

// If self-hosted, prompt for hostname
if hostingOption == HostingOptionSelfHosted {
hostnameForm := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("Enter hostname URL").
Placeholder("https://your-instance.example.com").
Value(&hostname),
),
)

if err := hostnameForm.Run(); err != nil {
logger.Error("Error: %v", err)
return
}
} else {
// For cloud, set the default hostname
hostname = "app.pangolin.net"
return err
}
} else {
// For cloud, set the default hostname
hostname = "app.pangolin.net"
}
}

// Normalize hostname (preserve protocol, remove trailing slash)
hostname = strings.TrimSuffix(hostname, "/")
// Normalize hostname (preserve protocol, remove trailing slash)
hostname = strings.TrimSuffix(hostname, "/")

// If no protocol specified, default to https
if !strings.HasPrefix(hostname, "http://") && !strings.HasPrefix(hostname, "https://") {
hostname = "https://" + hostname
}
// If no protocol specified, default to https
if !strings.HasPrefix(hostname, "http://") && !strings.HasPrefix(hostname, "https://") {
hostname = "https://" + hostname
}

// Perform web login
sessionToken, err := loginWithWeb(hostname)
if err != nil {
logger.Error("%v", err)
return
}
// Perform web login
sessionToken, err := loginWithWeb(hostname)
if err != nil {
logger.Error("%v", err)
return err
}

if sessionToken == "" {
logger.Error("Login appeared successful but no session token was received.")
return
}
if sessionToken == "" {
err := errors.New("login appeared successful but no session token was received.")
logger.Error("Error: %v", err)
return err
}

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

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

// Get user information
var user *api.User
user, err = apiClient.GetUser()
if err != nil {
logger.Error("Failed to get user information: %v", err)
return // FIXME: handle errors properly with exit codes!
}
// Get user information
var user *api.User
user, err = apiClient.GetUser()
if err != nil {
logger.Error("Failed to get user information: %v", err)
return err
}

if _, exists := accountStore.Accounts[user.UserID]; exists {
logger.Warning("Already logged in as this user; no action needed")
return
}
if _, exists := accountStore.Accounts[user.UserID]; exists {
logger.Warning("Already logged in as this user; no action needed")
return nil
}

// Ensure OLM credentials exist and are valid
userID := user.UserID
// 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
}
orgID, err := utils.SelectOrgForm(apiClient, userID)
if err != nil {
logger.Error("Failed to select organization: %v", err)
return err
}

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

newAccount := config.Account{
UserID: userID,
Host: hostname,
Email: user.Email,
SessionToken: sessionToken,
OrgID: orgID,
OlmCredentials: &config.OlmCredentials{
ID: newOlmCreds.OlmID,
Secret: newOlmCreds.Secret,
},
}
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
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
}
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 err
}

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

// Print logged in message after all setup is complete
if user != nil {
displayName := user.Email
if displayName == "" && user.Username != nil && *user.Username != "" {
displayName = *user.Username
}
if displayName != "" {
logger.Success("Logged in as %s", displayName)
}
// Print logged in message after all setup is complete
if user != nil {
displayName := user.Email
if displayName == "" && user.Username != nil && *user.Username != "" {
displayName = *user.Username
}
},
if displayName != "" {
logger.Success("Logged in as %s", displayName)
}
}

return nil
}
Loading