Skip to content

Commit 18ea14d

Browse files
committed
feat(account): support configuring credentials with browser login
1 parent 1646e59 commit 18ea14d

File tree

5 files changed

+131
-0
lines changed

5 files changed

+131
-0
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/UpCloudLtd/upcloud-go-api/v8 v8.16.0
88
github.com/adrg/xdg v0.3.2
99
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
10+
github.com/cli/browser v1.3.0
1011
github.com/gemalto/flume v0.12.0
1112
github.com/jedib0t/go-pretty/v6 v6.4.9
1213
github.com/m7shapan/cidr v0.0.0-20200427124835-7eba0889a5d2

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm
3939
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
4040
github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
4141
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
42+
github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
43+
github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
4244
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
4345
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
4446
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=

internal/commands/account/login.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import (
55
"fmt"
66
"strings"
77

8+
"github.com/UpCloudLtd/progress/messages"
89
"github.com/UpCloudLtd/upcloud-cli/v3/internal/commands"
10+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/commands/account/tokenreceiver"
911
"github.com/UpCloudLtd/upcloud-cli/v3/internal/config"
1012
"github.com/UpCloudLtd/upcloud-cli/v3/internal/output"
1113
"github.com/spf13/pflag"
@@ -47,6 +49,46 @@ func (s *loginCommand) ExecuteWithoutArguments(exec commands.Executor) (output.O
4749
return s.executeWithToken(exec)
4850
}
4951

52+
return s.execute(exec)
53+
}
54+
55+
func (s *loginCommand) execute(exec commands.Executor) (output.Output, error) {
56+
msg := "Waiting to receive token from browser."
57+
exec.PushProgressStarted(msg)
58+
59+
receiver := tokenreceiver.New()
60+
err := receiver.Start()
61+
if err != nil {
62+
return commands.HandleError(exec, msg, err)
63+
}
64+
65+
err = receiver.OpenBrowser()
66+
if err != nil {
67+
url := receiver.GetLoginURL()
68+
exec.PushProgressUpdate(messages.Update{
69+
Message: "Failed to open browser.",
70+
Status: messages.MessageStatusError,
71+
Details: fmt.Sprintf("Please open a browser and navigate to %s to continue with the login.", url),
72+
})
73+
}
74+
75+
token, err := receiver.Wait(exec.Context())
76+
if err != nil {
77+
return commands.HandleError(exec, msg, err)
78+
}
79+
80+
exec.PushProgressUpdate(messages.Update{
81+
Key: msg,
82+
Message: "Saving created token to the system keyring.",
83+
})
84+
85+
err = config.SaveTokenToKeyring(token)
86+
if err != nil {
87+
return commands.HandleError(exec, msg, fmt.Errorf("failed to save token to keyring: %w", err))
88+
}
89+
90+
exec.PushProgressSuccess(msg)
91+
5092
return output.None{}, nil
5193
}
5294

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package tokenreceiver
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net"
7+
"net/http"
8+
"time"
9+
10+
"github.com/cli/browser"
11+
)
12+
13+
type ReceiverServer struct {
14+
server *http.Server
15+
token string
16+
port string
17+
}
18+
19+
func New() *ReceiverServer {
20+
return &ReceiverServer{}
21+
}
22+
23+
func getPort(listener net.Listener) string {
24+
_, port, _ := net.SplitHostPort(listener.Addr().String())
25+
return port
26+
}
27+
28+
func getURL(target string) string {
29+
return fmt.Sprintf("http://localhost:3000/account/upctl-login/%s", target)
30+
}
31+
32+
func (s *ReceiverServer) GetLoginURL() string {
33+
return getURL(s.port)
34+
}
35+
36+
func (s *ReceiverServer) Start() error {
37+
handler := http.NewServeMux()
38+
handler.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
39+
token := req.URL.Query().Get("token")
40+
if token == "" {
41+
http.Redirect(w, req, getURL("error"), http.StatusSeeOther)
42+
return
43+
}
44+
s.token = token
45+
http.Redirect(w, req, getURL("success"), http.StatusSeeOther)
46+
})
47+
48+
listener, err := net.Listen("tcp", "127.0.0.1:0")
49+
if err != nil {
50+
return fmt.Errorf("failed to create receiver server: %w", err)
51+
}
52+
53+
go func() {
54+
defer listener.Close()
55+
s.server = &http.Server{
56+
Handler: handler,
57+
ReadHeaderTimeout: time.Second,
58+
}
59+
_ = s.server.Serve(listener)
60+
}()
61+
s.port = getPort(listener)
62+
return nil
63+
}
64+
65+
func (s *ReceiverServer) OpenBrowser() error {
66+
return browser.OpenURL(s.GetLoginURL())
67+
}
68+
69+
func (s *ReceiverServer) Wait(ctx context.Context) (string, error) {
70+
ticker := time.NewTicker(time.Second * 2)
71+
defer ticker.Stop()
72+
73+
for {
74+
select {
75+
case <-ctx.Done():
76+
_ = s.server.Shutdown(context.TODO())
77+
return "", ctx.Err()
78+
case <-ticker.C:
79+
if s.token != "" {
80+
_ = s.server.Shutdown(context.TODO())
81+
return s.token, nil
82+
}
83+
}
84+
}
85+
}

internal/commands/all/all.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ func BuildCommands(rootCmd *cobra.Command, conf *config.Config) {
117117
accountCommand := commands.BuildCommand(account.BaseAccountCommand(), rootCmd, conf)
118118
commands.BuildCommand(account.LoginCommand(), accountCommand.Cobra(), conf)
119119
commands.BuildCommand(account.ShowCommand(), accountCommand.Cobra(), conf)
120+
commands.BuildCommand(account.LoginCommand(), accountCommand.Cobra(), conf)
120121
commands.BuildCommand(account.ListCommand(), accountCommand.Cobra(), conf)
121122
commands.BuildCommand(account.DeleteCommand(), accountCommand.Cobra(), conf)
122123

0 commit comments

Comments
 (0)