Skip to content

Commit 0cb13d7

Browse files
authored
ZEA-4557: CLI: Use API token pattern instead of OAuth in CLI (#118)
#### Description (required) - **feat(constant): Add ZeaburDashURL constant** - **refactor(auth): Rewrite as API key client** - **refactor(auth): Switch to new API key client** - **refactor: Remove all OAuth related code** Demo: https://discord.com/channels/1060209568820494336/1079051435595026482/1337347559605145621 #### Related issues & labels (optional) - Closes ZEA-4557 - Suggested label: enhancement
2 parents e92ead3 + 6992b50 commit 0cb13d7

File tree

18 files changed

+165
-473
lines changed

18 files changed

+165
-473
lines changed

cmd/main.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
package main
33

44
import (
5+
"fmt"
56
"os"
67
"time"
78

@@ -54,7 +55,12 @@ func initFactory() *cmdutil.Factory {
5455

5556
factory.Printer = printer.New()
5657

57-
factory.AuthClient = auth.NewZeaburWebAppOAuthClient()
58+
cbs, err := auth.NewCallbackServer()
59+
if err != nil {
60+
panic(fmt.Sprintf("failed to create callback server (internal error): %v", err))
61+
}
62+
63+
factory.AuthClient = auth.NewImplicitFlowClient(cbs)
5864

5965
factory.Prompter = prompt.New()
6066

internal/cmd/auth/login/login.go

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"github.com/hasura/go-graphql-client"
1111
"github.com/spf13/cobra"
1212
"github.com/spf13/viper"
13-
"golang.org/x/oauth2"
1413

1514
"github.com/zeabur/cli/internal/cmdutil"
1615
"github.com/zeabur/cli/pkg/api"
@@ -70,8 +69,7 @@ func RunLogin(f *cmdutil.Factory, opts *Options) error {
7069
return fmt.Errorf("failed to get user info: %w", err)
7170
}
7271
} else {
73-
f.Log.Debugw("Already logged in", "token string", f.Config.GetTokenString(),
74-
"token detail", f.Config.GetToken(), "user", user)
72+
f.Log.Debugw("Already logged in", "token", f.Config.GetTokenString(), "user", user)
7573
f.Log.Infof("Already logged in as %s, "+
7674
"if you want to use a different account, please logout first", user.Name)
7775
return nil
@@ -80,29 +78,27 @@ func RunLogin(f *cmdutil.Factory, opts *Options) error {
8078

8179
var (
8280
tokenString string
83-
token *oauth2.Token
8481
err error
8582
)
8683

8784
if f.Interactive {
8885
f.Log.Info("A browser window will be opened for you to login, please confirm")
8986
// get token from web
90-
token, err = f.AuthClient.Login()
87+
token, err := f.AuthClient.GenerateToken(context.Background())
9188
if err != nil {
9289
return fmt.Errorf("failed to login: %w", err)
9390
}
94-
tokenString = token.AccessToken
95-
f.Config.SetToken(token)
91+
tokenString = token
92+
93+
f.Config.SetTokenString(tokenString)
9694
} else {
9795
// get token from flag, env or config
9896
if tokenString = f.Config.GetTokenString(); tokenString == "" {
9997
return fmt.Errorf("please set ZEABUR_TOKEN environment variable or use --token flag to set token")
10098
}
10199
}
102100

103-
f.Config.SetTokenString(tokenString)
104-
105-
f.Log.Debugw("Token", "token", tokenString, "token detail", token)
101+
f.Log.Debugw("Token", "token", tokenString)
106102

107103
// because we just logged in, we need to create a new client
108104
f.ApiClient = opts.NewClient(tokenString)

internal/cmd/auth/logout/logout.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,6 @@ func runLogout(f *cmdutil.Factory, opts *logoutOptions) error {
3737
f.Config.SetUser("")
3838
f.Config.SetUsername("")
3939

40-
// reset token detail if exists
41-
if f.Config.GetToken() != nil {
42-
f.Config.SetToken(nil)
43-
}
44-
4540
// reset context
4641
f.Config.GetContext().ClearAll()
4742

internal/cmd/deploy/deploy.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/spf13/cobra"
99
"github.com/zeabur/cli/internal/cmdutil"
1010
"github.com/zeabur/cli/internal/util"
11+
"github.com/zeabur/cli/pkg/constant"
1112
"github.com/zeabur/cli/pkg/model"
1213
"github.com/zeabur/cli/pkg/selector"
1314
"github.com/zeabur/cli/pkg/zcontext"
@@ -91,7 +92,7 @@ func runDeploy(f *cmdutil.Factory, opts *Options) error {
9192

9293
if domainName == "" {
9394
fmt.Println("Service deployed successfully, you can access it via:")
94-
fmt.Println("https://zeabur.com/projects/" + service.Project.ID + "/services/" + service.ID + "?envID=" + environment.ID)
95+
fmt.Println(constant.ZeaburDashURL + "/projects/" + service.Project.ID + "/services/" + service.ID + "?envID=" + environment.ID)
9596
return nil
9697
}
9798

internal/cmd/root/root.go

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22
package root
33

44
import (
5+
"context"
56
"fmt"
6-
"time"
77

88
"github.com/MakeNowJust/heredoc"
99
"github.com/spf13/cobra"
10-
"golang.org/x/oauth2"
1110

1211
authCmd "github.com/zeabur/cli/internal/cmd/auth"
1312
completionCmd "github.com/zeabur/cli/internal/cmd/completion"
@@ -56,16 +55,13 @@ func NewCmdRoot(f *cmdutil.Factory, version, commit, date string) (*cobra.Comman
5655

5756
var (
5857
tokenString string
59-
token *oauth2.Token
6058
err error
6159
)
6260

63-
token, err = f.AuthClient.Login()
61+
tokenString, err = f.AuthClient.GenerateToken(context.Background())
6462
if err != nil {
6563
return fmt.Errorf("failed to login: %w", err)
6664
}
67-
tokenString = token.AccessToken
68-
f.Config.SetToken(token)
6965
f.Config.SetTokenString(tokenString)
7066
}
7167
// set up the client
@@ -82,31 +78,6 @@ func NewCmdRoot(f *cmdutil.Factory, version, commit, date string) (*cobra.Comman
8278
return fmt.Errorf("failed to save config: %w", err)
8379
}
8480

85-
// refresh the token if the token is from OAuth2 & it's expired
86-
// skip help, version and auth commands
87-
if cmd.Name() != "help" && cmd.Name() != "version" && cmd.Parent().Name() != "auth" {
88-
if f.AutoRefreshToken && f.LoggedIn() && f.Config.GetToken() != nil {
89-
if f.Config.GetToken().Expiry.Before(time.Now()) {
90-
f.Log.Info("Token is from OAuth2 and it's expired, refreshing it")
91-
92-
token := f.Config.GetToken()
93-
token.Expiry = time.Now()
94-
newToken, err := f.AuthClient.RefreshToken(token)
95-
if err != nil {
96-
return fmt.Errorf("failed to refresh token, it is recommended to logout and login again: %w", err)
97-
}
98-
f.Config.SetToken(newToken)
99-
f.Config.SetTokenString(newToken.AccessToken)
100-
f.Log.Debug("New token: ", newToken)
101-
if err := f.Config.Write(); err != nil {
102-
return fmt.Errorf("failed to save config: %w", err)
103-
}
104-
105-
f.Log.Info("Token refreshed successfully")
106-
}
107-
}
108-
}
109-
11081
if f.AutoCheckUpdate && !f.Debug && version != "dev" {
11182
currentVersion := TrimPrefixV(version)
11283

@@ -140,8 +111,6 @@ func NewCmdRoot(f *cmdutil.Factory, version, commit, date string) (*cobra.Comman
140111
// Persistent flags
141112
cmd.PersistentFlags().BoolVar(&f.Debug, "debug", false, "Enable debug logging")
142113
cmd.PersistentFlags().BoolVarP(&f.Interactive, config.KeyInteractive, "i", true, "use interactive mode")
143-
cmd.PersistentFlags().BoolVar(&f.AutoRefreshToken, config.KeyAutoRefreshToken, true,
144-
"automatically refresh token when it's expired, only works when the token is from browser(OAuth2)")
145114
cmd.PersistentFlags().BoolVar(&f.AutoCheckUpdate, config.KeyAutoCheckUpdate, true, "automatically check update")
146115

147116
// Child commands

internal/cmd/service/deploy/deploy.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/spf13/cobra"
1010
"github.com/zeabur/cli/internal/cmdutil"
1111
"github.com/zeabur/cli/internal/util"
12+
"github.com/zeabur/cli/pkg/constant"
1213
)
1314

1415
type Options struct {
@@ -150,7 +151,7 @@ func runDeployInteractive(f *cmdutil.Factory, opts *Options) error {
150151
s.Stop()
151152

152153
fmt.Printf("%s Service %s created 🚀\n", cmdutil.SuccessIcon, service.Name)
153-
fmt.Printf("https://zeabur.com/projects/%s/services/%s", opts.projectID, service.ID)
154+
fmt.Printf("%s/projects/%s/services/%s", constant.ZeaburDashURL, opts.projectID, service.ID)
154155

155156
return nil
156157

internal/cmd/template/create/create.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"gopkg.in/yaml.v3"
1414

1515
"github.com/zeabur/cli/internal/cmdutil"
16+
"github.com/zeabur/cli/pkg/constant"
1617
"github.com/zeabur/cli/pkg/util"
1718
)
1819

@@ -102,6 +103,6 @@ func runCreate(f *cmdutil.Factory, opts Options) error {
102103
return err
103104
}
104105

105-
f.Log.Infof("Template %q (https://zeabur.com/templates/%s) created", t.Name, t.Code)
106+
f.Log.Infof("Template %q (%s/templates/%s) created", t.Name, constant.ZeaburDashURL, t.Code)
106107
return nil
107108
}

internal/cmd/template/deploy/deploy.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/hasura/go-graphql-client"
1515
"github.com/spf13/cobra"
1616
"github.com/zeabur/cli/internal/cmdutil"
17+
"github.com/zeabur/cli/pkg/constant"
1718
"github.com/zeabur/cli/pkg/model"
1819
"github.com/zeabur/cli/pkg/util"
1920
"gopkg.in/yaml.v3"
@@ -187,7 +188,7 @@ func runDeploy(f *cmdutil.Factory, opts *Options) error {
187188
return err
188189
}
189190

190-
f.Log.Infof("Template successfully deployed into project %q (https://zeabur.com/projects/%s).", res.Name, res.ID)
191+
f.Log.Infof("Template successfully deployed into project %q (%s/projects/%s).", res.Name, constant.ZeaburDashURL, res.ID)
191192

192193
if d, ok := vars["PUBLIC_DOMAIN"]; ok && project.Region.ID != "sha1" {
193194
s = spinner.New(cmdutil.SpinnerCharSet, cmdutil.SpinnerInterval,
@@ -201,7 +202,7 @@ func runDeploy(f *cmdutil.Factory, opts *Options) error {
201202
for {
202203
if time.Since(start) > 2*time.Minute {
203204
s.Stop()
204-
return fmt.Errorf("failed to wait service ready, check logs in https://zeabur.com/projects/%s", res.ID)
205+
return fmt.Errorf("failed to wait service ready, check logs in %s/projects/%s", constant.ZeaburDashURL, res.ID)
205206
}
206207

207208
time.Sleep(2 * time.Second)

pkg/auth/callback.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package auth
2+
3+
import (
4+
"context"
5+
_ "embed"
6+
"fmt"
7+
"net"
8+
"net/http"
9+
)
10+
11+
//go:embed login_success_page.html
12+
var loginSuccessHTML []byte
13+
14+
type CallbackServer struct {
15+
listener net.Listener
16+
tokenCh chan TokenResponse
17+
}
18+
19+
type TokenResponse struct {
20+
Token string
21+
State string
22+
}
23+
24+
func NewCallbackServer() (*CallbackServer, error) {
25+
listener, err := net.Listen("tcp4", "127.0.0.1:0")
26+
if err != nil {
27+
return nil, err
28+
}
29+
30+
tokenCh := make(chan TokenResponse, 1)
31+
32+
return &CallbackServer{
33+
listener: listener,
34+
tokenCh: tokenCh,
35+
}, nil
36+
}
37+
38+
func (s *CallbackServer) Serve() error {
39+
mux := http.NewServeMux()
40+
mux.HandleFunc("POST /callback", func(w http.ResponseWriter, r *http.Request) {
41+
s.tokenCh <- TokenResponse{
42+
Token: r.FormValue("api_key"),
43+
State: r.FormValue("state"),
44+
}
45+
46+
w.Header().Add("Content-Type", "text/html")
47+
48+
w.WriteHeader(200)
49+
_, _ = w.Write(loginSuccessHTML)
50+
})
51+
52+
return http.Serve(s.listener, mux)
53+
}
54+
55+
func (s *CallbackServer) Close() error {
56+
err := s.listener.Close()
57+
close(s.tokenCh)
58+
59+
return err
60+
}
61+
62+
func (s *CallbackServer) Port() int {
63+
return s.listener.Addr().(*net.TCPAddr).Port
64+
}
65+
66+
func (s *CallbackServer) GetCallbackURL() string {
67+
return fmt.Sprintf("http://localhost:%d/callback", s.Port())
68+
}
69+
70+
func (s *CallbackServer) WaitForToken(ctx context.Context) (TokenResponse, error) {
71+
select {
72+
case <-ctx.Done():
73+
return TokenResponse{}, ctx.Err()
74+
case token, ok := <-s.tokenCh:
75+
if !ok {
76+
return TokenResponse{}, fmt.Errorf("token channel closed")
77+
}
78+
79+
return token, nil
80+
}
81+
}

0 commit comments

Comments
 (0)