-
-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add auth support via command (#129)
Co-authored-by: Martin Kolárik <martin@kolarik.sk>
- Loading branch information
1 parent
e87cb79
commit abf3b23
Showing
42 changed files
with
1,732 additions
and
346 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
package cmd | ||
|
||
import ( | ||
"errors" | ||
"syscall" | ||
|
||
"github.com/jsdelivr/globalping-cli/globalping" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
func (r *Root) initAuth() { | ||
authCmd := &cobra.Command{ | ||
Use: "auth", | ||
Short: "Authenticate with the Globalping API", | ||
Long: "Authenticate with the Globalping API for higher measurements limits.", | ||
} | ||
|
||
loginCmd := &cobra.Command{ | ||
RunE: r.RunAuthLogin, | ||
Use: "login", | ||
Short: "Log in to your Globalping account", | ||
Long: `Log in to your Globalping account for higher measurements limits.`, | ||
} | ||
|
||
loginFlags := loginCmd.Flags() | ||
loginFlags.Bool("with-token", false, "authenticate with a token read from stdin instead of the default browser-based flow") | ||
|
||
statusCmd := &cobra.Command{ | ||
RunE: r.RunAuthStatus, | ||
Use: "status", | ||
Short: "Check the current authentication status", | ||
Long: `Check the current authentication status.`, | ||
} | ||
|
||
logoutCmd := &cobra.Command{ | ||
RunE: r.RunAuthLogout, | ||
Use: "logout", | ||
Short: "Log out from your Globalping account", | ||
Long: `Log out from your Globalping account.`, | ||
} | ||
|
||
authCmd.AddCommand(loginCmd) | ||
authCmd.AddCommand(statusCmd) | ||
authCmd.AddCommand(logoutCmd) | ||
|
||
r.Cmd.AddCommand(authCmd) | ||
} | ||
|
||
func (r *Root) RunAuthLogin(cmd *cobra.Command, args []string) error { | ||
var err error | ||
oldToken := r.storage.GetProfile().Token | ||
withToken := cmd.Flags().Changed("with-token") | ||
if withToken { | ||
err := r.loginWithToken() | ||
if err != nil { | ||
return err | ||
} | ||
if oldToken != nil { | ||
r.client.RevokeToken(oldToken.RefreshToken) | ||
} | ||
return nil | ||
} | ||
res, err := r.client.Authorize(func(e error) { | ||
defer func() { | ||
r.cancel <- syscall.SIGINT | ||
}() | ||
if e != nil { | ||
err = e | ||
r.Cmd.SilenceUsage = true | ||
return | ||
} | ||
if oldToken != nil { | ||
r.client.RevokeToken(oldToken.RefreshToken) | ||
} | ||
r.printer.Println("Success! You are now authenticated.") | ||
}) | ||
if err != nil { | ||
return err | ||
} | ||
r.printer.Println("Please visit the following URL to authenticate:") | ||
r.printer.Println(res.AuthorizeURL) | ||
r.utils.OpenBrowser(res.AuthorizeURL) | ||
r.printer.Println("\nCan't use the browser-based flow? Use \"globalping auth login --with-token\" to read a token from stdin instead.") | ||
<-r.cancel | ||
return err | ||
} | ||
|
||
func (r *Root) RunAuthStatus(cmd *cobra.Command, args []string) error { | ||
res, err := r.client.TokenIntrospection("") | ||
if err != nil { | ||
e, ok := err.(*globalping.AuthorizeError) | ||
if ok && e.ErrorType == "not_authorized" { | ||
r.printer.Println("Not logged in.") | ||
return nil | ||
} | ||
return err | ||
} | ||
if res.Active { | ||
r.printer.Printf("Logged in as %s.\n", res.Username) | ||
} else { | ||
r.printer.Println("Not logged in.") | ||
} | ||
return nil | ||
} | ||
|
||
func (r *Root) RunAuthLogout(cmd *cobra.Command, args []string) error { | ||
err := r.client.Logout() | ||
if err != nil { | ||
return err | ||
} | ||
r.printer.Println("You are now logged out.") | ||
return nil | ||
} | ||
|
||
func (r *Root) loginWithToken() error { | ||
r.printer.Println("Please enter your token:") | ||
token, err := r.printer.ReadPassword() | ||
if err != nil { | ||
return err | ||
} | ||
if token == "" { | ||
return errors.New("empty token") | ||
} | ||
introspection, err := r.client.TokenIntrospection(token) | ||
if err != nil { | ||
return err | ||
} | ||
if !introspection.Active { | ||
return errors.New("invalid token") | ||
} | ||
profile := r.storage.GetProfile() | ||
profile.Token = &globalping.Token{ | ||
AccessToken: token, | ||
} | ||
err = r.storage.SaveConfig() | ||
if err != nil { | ||
return errors.New("failed to save token") | ||
} | ||
r.printer.Printf("Logged in as %s.\n", introspection.Username) | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
package cmd | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"os" | ||
"syscall" | ||
"testing" | ||
|
||
"github.com/jsdelivr/globalping-cli/globalping" | ||
"github.com/jsdelivr/globalping-cli/mocks" | ||
"github.com/jsdelivr/globalping-cli/storage" | ||
"github.com/jsdelivr/globalping-cli/view" | ||
"github.com/stretchr/testify/assert" | ||
"go.uber.org/mock/gomock" | ||
) | ||
|
||
func Test_Auth_Login_WithToken(t *testing.T) { | ||
t.Cleanup(sessionCleanup) | ||
|
||
ctrl := gomock.NewController(t) | ||
defer ctrl.Finish() | ||
|
||
gbMock := mocks.NewMockClient(ctrl) | ||
|
||
w := new(bytes.Buffer) | ||
r := new(bytes.Buffer) | ||
r.WriteString("token\n") | ||
printer := view.NewPrinter(r, w, w) | ||
ctx := createDefaultContext("") | ||
_storage := storage.NewLocalStorage(".test_globalping-cli") | ||
defer _storage.Remove() | ||
err := _storage.Init() | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
_storage.GetProfile().Token = &globalping.Token{ | ||
AccessToken: "oldToken", | ||
RefreshToken: "oldRefreshToken", | ||
} | ||
|
||
root := NewRoot(printer, ctx, nil, nil, gbMock, nil, _storage) | ||
|
||
gbMock.EXPECT().TokenIntrospection("token").Return(&globalping.IntrospectionResponse{ | ||
Active: true, | ||
Username: "test", | ||
}, nil) | ||
gbMock.EXPECT().RevokeToken("oldRefreshToken").Return(nil) | ||
|
||
os.Args = []string{"globalping", "auth", "login", "--with-token"} | ||
err = root.Cmd.ExecuteContext(context.TODO()) | ||
assert.NoError(t, err) | ||
|
||
assert.Equal(t, `Please enter your token: | ||
Logged in as test. | ||
`, w.String()) | ||
|
||
profile := _storage.GetProfile() | ||
assert.Equal(t, &storage.Profile{ | ||
Token: &globalping.Token{ | ||
AccessToken: "token", | ||
}, | ||
}, profile) | ||
} | ||
|
||
func Test_Auth_Login(t *testing.T) { | ||
t.Cleanup(sessionCleanup) | ||
|
||
ctrl := gomock.NewController(t) | ||
defer ctrl.Finish() | ||
|
||
gbMock := mocks.NewMockClient(ctrl) | ||
utilsMock := mocks.NewMockUtils(ctrl) | ||
|
||
w := new(bytes.Buffer) | ||
printer := view.NewPrinter(nil, w, w) | ||
ctx := createDefaultContext("") | ||
_storage := storage.NewLocalStorage(".test_globalping-cli") | ||
defer _storage.Remove() | ||
err := _storage.Init() | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
_storage.GetProfile().Token = &globalping.Token{ | ||
AccessToken: "oldToken", | ||
RefreshToken: "oldRefreshToken", | ||
} | ||
|
||
root := NewRoot(printer, ctx, nil, utilsMock, gbMock, nil, _storage) | ||
|
||
gbMock.EXPECT().Authorize(gomock.Any()).Do(func(_ any) { | ||
root.cancel <- syscall.SIGINT | ||
}).Return(&globalping.AuthorizeResponse{ | ||
AuthorizeURL: "http://localhost", | ||
}, nil) | ||
utilsMock.EXPECT().OpenBrowser("http://localhost").Return(nil) | ||
|
||
os.Args = []string{"globalping", "auth", "login"} | ||
err = root.Cmd.ExecuteContext(context.TODO()) | ||
assert.NoError(t, err) | ||
|
||
assert.Equal(t, `Please visit the following URL to authenticate: | ||
http://localhost | ||
Can't use the browser-based flow? Use "globalping auth login --with-token" to read a token from stdin instead. | ||
`, w.String()) | ||
} | ||
|
||
func Test_AuthStatus(t *testing.T) { | ||
t.Cleanup(sessionCleanup) | ||
|
||
ctrl := gomock.NewController(t) | ||
defer ctrl.Finish() | ||
|
||
gbMock := mocks.NewMockClient(ctrl) | ||
|
||
w := new(bytes.Buffer) | ||
printer := view.NewPrinter(nil, w, w) | ||
ctx := createDefaultContext("") | ||
|
||
root := NewRoot(printer, ctx, nil, nil, gbMock, nil, nil) | ||
|
||
gbMock.EXPECT().TokenIntrospection("").Return(&globalping.IntrospectionResponse{ | ||
Active: true, | ||
Username: "test", | ||
}, nil) | ||
|
||
os.Args = []string{"globalping", "auth", "status"} | ||
err := root.Cmd.ExecuteContext(context.TODO()) | ||
assert.NoError(t, err) | ||
|
||
assert.Equal(t, `Logged in as test. | ||
`, w.String()) | ||
} | ||
|
||
func Test_Logout(t *testing.T) { | ||
t.Cleanup(sessionCleanup) | ||
|
||
ctrl := gomock.NewController(t) | ||
defer ctrl.Finish() | ||
|
||
gbMock := mocks.NewMockClient(ctrl) | ||
|
||
w := new(bytes.Buffer) | ||
printer := view.NewPrinter(nil, w, w) | ||
ctx := createDefaultContext("") | ||
|
||
root := NewRoot(printer, ctx, nil, nil, gbMock, nil, nil) | ||
|
||
gbMock.EXPECT().Logout().Return(nil) | ||
|
||
os.Args = []string{"globalping", "auth", "logout"} | ||
err := root.Cmd.ExecuteContext(context.TODO()) | ||
assert.NoError(t, err) | ||
|
||
assert.Equal(t, "You are now logged out.\n", w.String()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.