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
4 changes: 2 additions & 2 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ linters:
rules:
- text: "instead of using struct literal"
linters:
- revive
- revive
- text: "should have a package comment"
linters:
- revive
Expand All @@ -38,7 +38,7 @@ linters:
- revive
- text: "time-naming"
linters:
- revive
- revive
- text: "error strings should not be capitalized or end with punctuation or a newline"
linters:
- revive
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ checks:
getdeps:
@mkdir -p ${GOPATH}/bin
@echo "Installing golangci-lint $(GOLANGCI_LINT_VERSION)"
# Will need to make it more error prone in future!
@curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin $(GOLANGCI_LINT_VERSION)

crosscompile:
Expand Down
185 changes: 185 additions & 0 deletions cmd/login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// Copyright (c) 2024 Parseable, Inc
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package cmd

import (
"bufio"
"fmt"
"os"
"os/exec"
"pb/pkg/config"
"runtime"
"strings"

"github.com/spf13/cobra"
)

const defaultCloudURL = "https://staging.parseable.com:8000"

var (
loginToken string
loginURL string
loginUsername string
loginPassword string
loginProfileName string
)

func init() {
LoginCmd.Flags().StringVar(&loginToken, "token", "", "Auth token for cloud login")
LoginCmd.Flags().StringVar(&loginURL, "url", "", "Server URL for self-hosted Parseable")
LoginCmd.Flags().StringVar(&loginUsername, "username", "", "Username for self-hosted login")
LoginCmd.Flags().StringVar(&loginPassword, "password", "", "Password for self-hosted login")
LoginCmd.Flags().StringVar(&loginProfileName, "profile", "default", "Profile name to save as")
}

var LoginCmd = &cobra.Command{
Use: "login",
Short: "Login to Parseable",
Long: `Login to Parseable cloud or a self-hosted instance.

Cloud login (opens browser):
pb login

Cloud login with token:
pb login --token <token>

Self-hosted login:
pb login --url http://localhost:8000 --username admin --password admin`,
RunE: func(_ *cobra.Command, _ []string) error {
// --- Self-hosted path ---
if loginURL != "" {
return selfHostedLogin()
}

// --- Cloud path ---
return cloudLogin()
},
}

func selfHostedLogin() error {
username := loginUsername
password := loginPassword

if username == "" {
fmt.Print("Username: ")
reader := bufio.NewReader(os.Stdin)
line, err := reader.ReadString('\n')
if err != nil {
return fmt.Errorf("failed to read username: %w", err)
}
username = strings.TrimSpace(line)
}

if password == "" {
fmt.Print("Password: ")
reader := bufio.NewReader(os.Stdin)
line, err := reader.ReadString('\n')
if err != nil {
return fmt.Errorf("failed to read password: %w", err)
}
password = strings.TrimSpace(line)
}

if username == "" || password == "" {
return fmt.Errorf("username and password are required for self-hosted login")
}

profile := config.Profile{
URL: loginURL,
Username: username,
Password: password,
}
if err := writeProfile(profile, loginProfileName); err != nil {
return fmt.Errorf("failed to save profile: %w", err)
}

fmt.Printf("✓ Logged in. Profile '%s' saved.\n", loginProfileName)
fmt.Printf(" URL: %s\n", loginURL)
return nil
}

func cloudLogin() error {
token := loginToken

if token == "" {
loginPageURL := defaultCloudURL + "/login"
fmt.Printf("Opening login page: %s\n\n", loginPageURL)

if err := openBrowser(loginPageURL); err != nil {
fmt.Println("Could not open browser automatically. Please visit the URL above and copy your token.")
} else {
fmt.Println("Browser opened. After logging in, copy your token from the dashboard.")
}

fmt.Print("\nPaste your token here: ")
reader := bufio.NewReader(os.Stdin)
line, err := reader.ReadString('\n')
if err != nil {
return fmt.Errorf("failed to read token: %w", err)
}
token = strings.TrimSpace(line)
if token == "" {
return fmt.Errorf("no token provided, login canceled")
}
}

profile := config.Profile{
URL: defaultCloudURL,
Token: token,
}
if err := writeProfile(profile, loginProfileName); err != nil {
return fmt.Errorf("failed to save profile: %w", err)
}

fmt.Printf("✓ Logged in. Profile '%s' saved.\n", loginProfileName)
fmt.Printf(" URL: %s\n", defaultCloudURL)
return nil
}

func writeProfile(profile config.Profile, profileName string) error {
fileConfig, err := config.ReadConfigFromFile()
if err != nil {
newConfig := config.Config{
Profiles: map[string]config.Profile{profileName: profile},
DefaultProfile: profileName,
}
return config.WriteConfigToFile(&newConfig)
}

if fileConfig.Profiles == nil {
fileConfig.Profiles = make(map[string]config.Profile)
}
fileConfig.Profiles[profileName] = profile
if fileConfig.DefaultProfile == "" {
fileConfig.DefaultProfile = profileName
}
return config.WriteConfigToFile(fileConfig)
}

func openBrowser(url string) error {
var cmd *exec.Cmd
switch runtime.GOOS {
case "darwin":
cmd = exec.Command("open", url)
case "linux":
cmd = exec.Command("xdg-open", url)
case "windows":
cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url)
default:
return fmt.Errorf("unsupported platform: %s", runtime.GOOS)
}
return cmd.Start()
}
51 changes: 51 additions & 0 deletions cmd/logout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) 2024 Parseable, Inc
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package cmd

import (
"fmt"
"pb/pkg/config"

"github.com/spf13/cobra"
)

var LogoutCmd = &cobra.Command{
Use: "logout",
Short: "Logout from the current Parseable profile",
Long: "Removes the active profile (URL and credentials) from config.",
Example: " pb logout",
RunE: func(_ *cobra.Command, _ []string) error {
fileConfig, err := config.ReadConfigFromFile()
if err != nil {
return fmt.Errorf("no config found — nothing to logout from")
}

profileName := fileConfig.DefaultProfile
if _, exists := fileConfig.Profiles[profileName]; !exists {
return fmt.Errorf("no active profile found")
}

delete(fileConfig.Profiles, profileName)
fileConfig.DefaultProfile = ""

if err := config.WriteConfigToFile(fileConfig); err != nil {
return fmt.Errorf("failed to update config: %w", err)
}

fmt.Printf("Logged out and removed profile '%s'\n", profileName)
return nil
},
}
4 changes: 2 additions & 2 deletions cmd/pre.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ func PreRunDefaultProfile(_ *cobra.Command, _ []string) error {
func PreRun() error {
conf, err := config.ReadConfigFromFile()
if os.IsNotExist(err) {
return errors.New("no config found to run this command. add a profile using pb profile command")
return errors.New("no profile configured. run: pb login")
} else if err != nil {
return err
}

if conf.Profiles == nil || conf.DefaultProfile == "" {
return errors.New("no profile is configured to run this command. please create one using profile command")
return errors.New("no profile configured. run: pb login")
}

DefaultProfile = conf.Profiles[conf.DefaultProfile]
Expand Down
58 changes: 58 additions & 0 deletions cmd/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) 2024 Parseable, Inc
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package cmd

import (
"fmt"
"pb/pkg/analytics"
"pb/pkg/config"
internalHTTP "pb/pkg/http"

"github.com/spf13/cobra"
)

var StatusCmd = &cobra.Command{
Use: "status",
Short: "Check connection status for the active profile",
Example: " pb status",
RunE: func(_ *cobra.Command, _ []string) error {
fileConfig, err := config.ReadConfigFromFile()
if err != nil {
return fmt.Errorf("no profile configured. run: pb login")
}

profileName := fileConfig.DefaultProfile
profile, exists := fileConfig.Profiles[profileName]
if !exists || profileName == "" {
return fmt.Errorf("no active profile. run: pb login")
}

fmt.Printf("Profile : %s\n", profileName)
fmt.Printf("URL : %s\n", profile.URL)

client := internalHTTP.DefaultClient(&profile)
about, err := analytics.FetchAbout(&client)
if err != nil {
fmt.Printf("Status : ✗ Not connected\n")
fmt.Printf("Error : %s\n", err.Error())
return nil
}

fmt.Printf("Status : ✓ Connected\n")
fmt.Printf("Version : %s\n", about.Version)
return nil
},
}
Loading
Loading