Skip to content

Commit b81fc96

Browse files
authored
feat: add login/logout/status commands (#91)
1 parent 25fc015 commit b81fc96

11 files changed

Lines changed: 330 additions & 68 deletions

File tree

‎.golangci.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ linters:
2929
rules:
3030
- text: "instead of using struct literal"
3131
linters:
32-
- revive
32+
- revive
3333
- text: "should have a package comment"
3434
linters:
3535
- revive
@@ -38,7 +38,7 @@ linters:
3838
- revive
3939
- text: "time-naming"
4040
linters:
41-
- revive
41+
- revive
4242
- text: "error strings should not be capitalized or end with punctuation or a newline"
4343
linters:
4444
- revive

‎Makefile‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ checks:
1919
getdeps:
2020
@mkdir -p ${GOPATH}/bin
2121
@echo "Installing golangci-lint $(GOLANGCI_LINT_VERSION)"
22+
# Will need to make it more error prone in future!
2223
@curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin $(GOLANGCI_LINT_VERSION)
2324

2425
crosscompile:

‎cmd/login.go‎

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// Copyright (c) 2024 Parseable, Inc
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU Affero General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// This program is distributed in the hope that it will be useful
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU Affero General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU Affero General Public License
14+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
16+
package cmd
17+
18+
import (
19+
"bufio"
20+
"fmt"
21+
"os"
22+
"os/exec"
23+
"pb/pkg/config"
24+
"runtime"
25+
"strings"
26+
27+
"github.com/spf13/cobra"
28+
)
29+
30+
const defaultCloudURL = "https://staging.parseable.com:8000"
31+
32+
var (
33+
loginToken string
34+
loginURL string
35+
loginUsername string
36+
loginPassword string
37+
loginProfileName string
38+
)
39+
40+
func init() {
41+
LoginCmd.Flags().StringVar(&loginToken, "token", "", "Auth token for cloud login")
42+
LoginCmd.Flags().StringVar(&loginURL, "url", "", "Server URL for self-hosted Parseable")
43+
LoginCmd.Flags().StringVar(&loginUsername, "username", "", "Username for self-hosted login")
44+
LoginCmd.Flags().StringVar(&loginPassword, "password", "", "Password for self-hosted login")
45+
LoginCmd.Flags().StringVar(&loginProfileName, "profile", "default", "Profile name to save as")
46+
}
47+
48+
var LoginCmd = &cobra.Command{
49+
Use: "login",
50+
Short: "Login to Parseable",
51+
Long: `Login to Parseable cloud or a self-hosted instance.
52+
53+
Cloud login (opens browser):
54+
pb login
55+
56+
Cloud login with token:
57+
pb login --token <token>
58+
59+
Self-hosted login:
60+
pb login --url http://localhost:8000 --username admin --password admin`,
61+
RunE: func(_ *cobra.Command, _ []string) error {
62+
// --- Self-hosted path ---
63+
if loginURL != "" {
64+
return selfHostedLogin()
65+
}
66+
67+
// --- Cloud path ---
68+
return cloudLogin()
69+
},
70+
}
71+
72+
func selfHostedLogin() error {
73+
username := loginUsername
74+
password := loginPassword
75+
76+
if username == "" {
77+
fmt.Print("Username: ")
78+
reader := bufio.NewReader(os.Stdin)
79+
line, err := reader.ReadString('\n')
80+
if err != nil {
81+
return fmt.Errorf("failed to read username: %w", err)
82+
}
83+
username = strings.TrimSpace(line)
84+
}
85+
86+
if password == "" {
87+
fmt.Print("Password: ")
88+
reader := bufio.NewReader(os.Stdin)
89+
line, err := reader.ReadString('\n')
90+
if err != nil {
91+
return fmt.Errorf("failed to read password: %w", err)
92+
}
93+
password = strings.TrimSpace(line)
94+
}
95+
96+
if username == "" || password == "" {
97+
return fmt.Errorf("username and password are required for self-hosted login")
98+
}
99+
100+
profile := config.Profile{
101+
URL: loginURL,
102+
Username: username,
103+
Password: password,
104+
}
105+
if err := writeProfile(profile, loginProfileName); err != nil {
106+
return fmt.Errorf("failed to save profile: %w", err)
107+
}
108+
109+
fmt.Printf("✓ Logged in. Profile '%s' saved.\n", loginProfileName)
110+
fmt.Printf(" URL: %s\n", loginURL)
111+
return nil
112+
}
113+
114+
func cloudLogin() error {
115+
token := loginToken
116+
117+
if token == "" {
118+
loginPageURL := defaultCloudURL + "/login"
119+
fmt.Printf("Opening login page: %s\n\n", loginPageURL)
120+
121+
if err := openBrowser(loginPageURL); err != nil {
122+
fmt.Println("Could not open browser automatically. Please visit the URL above and copy your token.")
123+
} else {
124+
fmt.Println("Browser opened. After logging in, copy your token from the dashboard.")
125+
}
126+
127+
fmt.Print("\nPaste your token here: ")
128+
reader := bufio.NewReader(os.Stdin)
129+
line, err := reader.ReadString('\n')
130+
if err != nil {
131+
return fmt.Errorf("failed to read token: %w", err)
132+
}
133+
token = strings.TrimSpace(line)
134+
if token == "" {
135+
return fmt.Errorf("no token provided, login canceled")
136+
}
137+
}
138+
139+
profile := config.Profile{
140+
URL: defaultCloudURL,
141+
Token: token,
142+
}
143+
if err := writeProfile(profile, loginProfileName); err != nil {
144+
return fmt.Errorf("failed to save profile: %w", err)
145+
}
146+
147+
fmt.Printf("✓ Logged in. Profile '%s' saved.\n", loginProfileName)
148+
fmt.Printf(" URL: %s\n", defaultCloudURL)
149+
return nil
150+
}
151+
152+
func writeProfile(profile config.Profile, profileName string) error {
153+
fileConfig, err := config.ReadConfigFromFile()
154+
if err != nil {
155+
newConfig := config.Config{
156+
Profiles: map[string]config.Profile{profileName: profile},
157+
DefaultProfile: profileName,
158+
}
159+
return config.WriteConfigToFile(&newConfig)
160+
}
161+
162+
if fileConfig.Profiles == nil {
163+
fileConfig.Profiles = make(map[string]config.Profile)
164+
}
165+
fileConfig.Profiles[profileName] = profile
166+
if fileConfig.DefaultProfile == "" {
167+
fileConfig.DefaultProfile = profileName
168+
}
169+
return config.WriteConfigToFile(fileConfig)
170+
}
171+
172+
func openBrowser(url string) error {
173+
var cmd *exec.Cmd
174+
switch runtime.GOOS {
175+
case "darwin":
176+
cmd = exec.Command("open", url)
177+
case "linux":
178+
cmd = exec.Command("xdg-open", url)
179+
case "windows":
180+
cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url)
181+
default:
182+
return fmt.Errorf("unsupported platform: %s", runtime.GOOS)
183+
}
184+
return cmd.Start()
185+
}

‎cmd/logout.go‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) 2024 Parseable, Inc
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU Affero General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// This program is distributed in the hope that it will be useful
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU Affero General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU Affero General Public License
14+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
16+
package cmd
17+
18+
import (
19+
"fmt"
20+
"pb/pkg/config"
21+
22+
"github.com/spf13/cobra"
23+
)
24+
25+
var LogoutCmd = &cobra.Command{
26+
Use: "logout",
27+
Short: "Logout from the current Parseable profile",
28+
Long: "Removes the active profile (URL and credentials) from config.",
29+
Example: " pb logout",
30+
RunE: func(_ *cobra.Command, _ []string) error {
31+
fileConfig, err := config.ReadConfigFromFile()
32+
if err != nil {
33+
return fmt.Errorf("no config found — nothing to logout from")
34+
}
35+
36+
profileName := fileConfig.DefaultProfile
37+
if _, exists := fileConfig.Profiles[profileName]; !exists {
38+
return fmt.Errorf("no active profile found")
39+
}
40+
41+
delete(fileConfig.Profiles, profileName)
42+
fileConfig.DefaultProfile = ""
43+
44+
if err := config.WriteConfigToFile(fileConfig); err != nil {
45+
return fmt.Errorf("failed to update config: %w", err)
46+
}
47+
48+
fmt.Printf("Logged out and removed profile '%s'\n", profileName)
49+
return nil
50+
},
51+
}

‎cmd/pre.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@ func PreRunDefaultProfile(_ *cobra.Command, _ []string) error {
3535
func PreRun() error {
3636
conf, err := config.ReadConfigFromFile()
3737
if os.IsNotExist(err) {
38-
return errors.New("no config found to run this command. add a profile using pb profile command")
38+
return errors.New("no profile configured. run: pb login")
3939
} else if err != nil {
4040
return err
4141
}
4242

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

4747
DefaultProfile = conf.Profiles[conf.DefaultProfile]

‎cmd/status.go‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (c) 2024 Parseable, Inc
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU Affero General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// This program is distributed in the hope that it will be useful
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU Affero General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU Affero General Public License
14+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
16+
package cmd
17+
18+
import (
19+
"fmt"
20+
"pb/pkg/analytics"
21+
"pb/pkg/config"
22+
internalHTTP "pb/pkg/http"
23+
24+
"github.com/spf13/cobra"
25+
)
26+
27+
var StatusCmd = &cobra.Command{
28+
Use: "status",
29+
Short: "Check connection status for the active profile",
30+
Example: " pb status",
31+
RunE: func(_ *cobra.Command, _ []string) error {
32+
fileConfig, err := config.ReadConfigFromFile()
33+
if err != nil {
34+
return fmt.Errorf("no profile configured. run: pb login")
35+
}
36+
37+
profileName := fileConfig.DefaultProfile
38+
profile, exists := fileConfig.Profiles[profileName]
39+
if !exists || profileName == "" {
40+
return fmt.Errorf("no active profile. run: pb login")
41+
}
42+
43+
fmt.Printf("Profile : %s\n", profileName)
44+
fmt.Printf("URL : %s\n", profile.URL)
45+
46+
client := internalHTTP.DefaultClient(&profile)
47+
about, err := analytics.FetchAbout(&client)
48+
if err != nil {
49+
fmt.Printf("Status : ✗ Not connected\n")
50+
fmt.Printf("Error : %s\n", err.Error())
51+
return nil
52+
}
53+
54+
fmt.Printf("Status : ✓ Connected\n")
55+
fmt.Printf("Version : %s\n", about.Version)
56+
return nil
57+
},
58+
}

0 commit comments

Comments
 (0)