Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Commit 11c83e4

Browse files
authored
feat: simplify login flow (#274)
1 parent 6e89477 commit 11c83e4

File tree

5 files changed

+43
-595
lines changed

5 files changed

+43
-595
lines changed

internal/cmd/login.go

Lines changed: 43 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
package cmd
22

33
import (
4+
"bufio"
45
"context"
56
"fmt"
6-
"net"
7-
"net/http"
87
"net/url"
8+
"os"
99
"strings"
1010

1111
"github.com/pkg/browser"
1212
"github.com/spf13/cobra"
13-
"golang.org/x/sync/errgroup"
1413
"golang.org/x/xerrors"
1514

1615
"cdr.dev/coder-cli/coder-sdk"
1716
"cdr.dev/coder-cli/internal/config"
18-
"cdr.dev/coder-cli/internal/loginsrv"
1917
"cdr.dev/coder-cli/internal/version"
2018
"cdr.dev/coder-cli/internal/x/xcobra"
2119
"cdr.dev/coder-cli/pkg/clog"
@@ -42,25 +40,55 @@ func loginCmd() *cobra.Command {
4240
// From this point, the commandline is correct.
4341
// Don't return errors as it would print the usage.
4442

45-
if err := login(cmd, u, config.URL, config.Session); err != nil {
43+
if err := login(cmd.Context(), u); err != nil {
4644
return xerrors.Errorf("login error: %w", err)
4745
}
4846
return nil
4947
},
5048
}
5149
}
5250

53-
// newLocalListener creates up a local tcp server using port 0 (i.e. any available port).
54-
// If ipv4 is disabled, try ipv6.
55-
// It will be used by the http server waiting for the auth callback.
56-
func newLocalListener() (net.Listener, error) {
57-
l, err := net.Listen("tcp", "127.0.0.1:0")
58-
if err != nil {
59-
if l, err = net.Listen("tcp6", "[::1]:0"); err != nil {
60-
return nil, xerrors.Errorf("listen on a port: %w", err)
61-
}
51+
// storeConfig writes the env URL and session token to the local config directory.
52+
// The config lib will handle the local config path lookup and creation.
53+
func storeConfig(envURL *url.URL, sessionToken string, urlCfg, sessionCfg config.File) error {
54+
if err := urlCfg.Write(envURL.String()); err != nil {
55+
return xerrors.Errorf("store env url: %w", err)
56+
}
57+
if err := sessionCfg.Write(sessionToken); err != nil {
58+
return xerrors.Errorf("store session token: %w", err)
6259
}
63-
return l, nil
60+
return nil
61+
}
62+
63+
func login(ctx context.Context, envURL *url.URL) error {
64+
authURL := *envURL
65+
authURL.Path = envURL.Path + "/internal-auth"
66+
q := authURL.Query()
67+
q.Add("show_token", "true")
68+
authURL.RawQuery = q.Encode()
69+
70+
if err := browser.OpenURL(authURL.String()); err != nil || true {
71+
fmt.Printf("Open the following in your browser:\n\n\t%s\n\n", authURL.String())
72+
} else {
73+
fmt.Printf("Your browser has been opened to visit:\n\n\t%s\n\n", authURL.String())
74+
}
75+
76+
token := readLine("Paste token here: ")
77+
if err := pingAPI(ctx, envURL, token); err != nil {
78+
return xerrors.Errorf("ping API with credentials: %w", err)
79+
}
80+
if err := storeConfig(envURL, token, config.URL, config.Session); err != nil {
81+
return xerrors.Errorf("store auth: %w", err)
82+
}
83+
clog.LogSuccess("logged in")
84+
return nil
85+
}
86+
87+
func readLine(prompt string) string {
88+
reader := bufio.NewReader(os.Stdin)
89+
fmt.Print(prompt)
90+
text, _ := reader.ReadString('\n')
91+
return strings.TrimSuffix(text, "\n")
6492
}
6593

6694
// pingAPI creates a client from the given url/token and try to exec an api call.
@@ -85,77 +113,3 @@ func pingAPI(ctx context.Context, envURL *url.URL, token string) error {
85113
}
86114
return nil
87115
}
88-
89-
// storeConfig writes the env URL and session token to the local config directory.
90-
// The config lib will handle the local config path lookup and creation.
91-
func storeConfig(envURL *url.URL, sessionToken string, urlCfg, sessionCfg config.File) error {
92-
if err := urlCfg.Write(envURL.String()); err != nil {
93-
return xerrors.Errorf("store env url: %w", err)
94-
}
95-
if err := sessionCfg.Write(sessionToken); err != nil {
96-
return xerrors.Errorf("store session token: %w", err)
97-
}
98-
return nil
99-
}
100-
101-
func login(cmd *cobra.Command, envURL *url.URL, urlCfg, sessionCfg config.File) error {
102-
ctx := cmd.Context()
103-
104-
// Start by creating the listener so we can prompt the user with the URL.
105-
listener, err := newLocalListener()
106-
if err != nil {
107-
return xerrors.Errorf("create local listener: %w", err)
108-
}
109-
defer func() { _ = listener.Close() }() // Best effort.
110-
111-
// Forge the auth URL with the callback set to the local server.
112-
authURL := *envURL
113-
authURL.Path = envURL.Path + "/internal-auth"
114-
authURL.RawQuery = "local_service=http://" + listener.Addr().String()
115-
116-
// Try to open the browser on the local computer.
117-
if err := browser.OpenURL(authURL.String()); err != nil {
118-
// Discard the error as it is an expected one in non-X environments like over ssh.
119-
// Tell the user to visit the URL instead.
120-
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Visit the following URL in your browser:\n\n\t%s\n\n", &authURL) // Can't fail.
121-
}
122-
123-
// Create our channel, it is going to be the central synchronization of the command.
124-
tokenChan := make(chan string)
125-
126-
// Create the http server outside the errgroup goroutine scope so we can stop it later.
127-
srv := &http.Server{Handler: &loginsrv.Server{TokenChan: tokenChan}}
128-
defer func() { _ = srv.Close() }() // Best effort. Direct close as we are dealing with a one-off request.
129-
130-
// Start both the readline and http server in parallel. As they are both long-running routines,
131-
// to know when to continue, we don't wait on the errgroup, but on the tokenChan.
132-
group, ctx := errgroup.WithContext(ctx)
133-
group.Go(func() error { return srv.Serve(listener) })
134-
group.Go(func() error { return loginsrv.ReadLine(ctx, cmd.InOrStdin(), cmd.ErrOrStderr(), tokenChan) })
135-
136-
// Only close then tokenChan when the errgroup is done. Best effort basis.
137-
// Will not return the http route is used with a regular terminal.
138-
// Useful for non interactive session, manual input, tests or custom stdin.
139-
go func() { defer close(tokenChan); _ = group.Wait() }()
140-
141-
var token string
142-
select {
143-
case <-ctx.Done():
144-
return ctx.Err()
145-
case token = <-tokenChan:
146-
}
147-
148-
// Perform an API call to verify that the token is valid.
149-
if err := pingAPI(ctx, envURL, token); err != nil {
150-
return xerrors.Errorf("ping API: %w", err)
151-
}
152-
153-
// Success. Store the config only at this point so we don't override the local one in case of failure.
154-
if err := storeConfig(envURL, token, urlCfg, sessionCfg); err != nil {
155-
return xerrors.Errorf("store config: %w", err)
156-
}
157-
158-
clog.LogSuccess("logged in")
159-
160-
return nil
161-
}

internal/loginsrv/input.go

Lines changed: 0 additions & 60 deletions
This file was deleted.

internal/loginsrv/input_test.go

Lines changed: 0 additions & 126 deletions
This file was deleted.

0 commit comments

Comments
 (0)