这是indexloc提供的服务,不要输入任何密码
Skip to content

Prep before SSO login flow work #798

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Mar 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 4 additions & 12 deletions cli/cmd/turbo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import (
"runtime/debug"
"strings"
"time"

"github.com/vercel/turborepo/cli/internal/config"
"github.com/vercel/turborepo/cli/internal/info"
"github.com/vercel/turborepo/cli/internal/login"
"github.com/vercel/turborepo/cli/internal/process"
prune "github.com/vercel/turborepo/cli/internal/prune"
"github.com/vercel/turborepo/cli/internal/run"
"github.com/vercel/turborepo/cli/internal/ui"
uiPkg "github.com/vercel/turborepo/cli/internal/ui"
"github.com/vercel/turborepo/cli/internal/util"

Expand Down Expand Up @@ -45,17 +47,7 @@ func main() {
c := cli.NewCLI("turbo", turboVersion)

util.InitPrintf()
ui := &cli.ColoredUi{
Ui: &cli.BasicUi{
Reader: os.Stdin,
Writer: os.Stdout,
ErrorWriter: os.Stderr,
},
OutputColor: cli.UiColorNone,
InfoColor: cli.UiColorNone,
WarnColor: cli.UiColorYellow,
ErrorColor: cli.UiColorRed,
}
ui := ui.Default()

c.Args = args
c.HelpWriter = os.Stdout
Expand Down Expand Up @@ -92,7 +84,7 @@ func main() {
return &login.UnlinkCommand{Config: cf, Ui: ui}, nil
},
"login": func() (cli.Command, error) {
return &login.LoginCommand{Config: cf, Ui: ui}, nil
return &login.LoginCommand{Config: cf, UI: ui}, nil
},
"logout": func() (cli.Command, error) {
return &login.LogoutCommand{Config: cf, Ui: ui}, nil
Expand Down
16 changes: 5 additions & 11 deletions cli/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,35 @@ require (
github.com/AlecAivazis/survey/v2 v2.2.12
github.com/Masterminds/semver v1.5.0
github.com/adrg/xdg v0.3.3
github.com/armon/go-radix v1.0.0 // indirect
github.com/bmatcuk/doublestar/v4 v4.0.2
github.com/briandowns/spinner v1.16.0
github.com/deckarep/golang-set v1.7.1
github.com/fatih/color v1.7.0
github.com/fatih/color v1.13.0
github.com/gobwas/glob v0.2.3
github.com/google/chrometracing v0.0.0-20210413150014-55fded0163e7
github.com/google/go-cmp v0.5.5 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/google/uuid v1.3.0
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-gatedio v0.5.0
github.com/hashicorp/go-hclog v1.1.0
github.com/hashicorp/go-retryablehttp v0.6.8
github.com/karrick/godirwalk v1.16.1
github.com/kelseyhightower/envconfig v1.4.0
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-isatty v0.0.12
github.com/mattn/go-isatty v0.0.14
github.com/mitchellh/cli v1.1.2
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/mitchellh/mapstructure v1.4.3
github.com/mitchellh/reflectwalk v1.0.1 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/pkg/errors v0.9.1
github.com/posener/complete v1.2.1 // indirect
github.com/pyr-sh/dag v1.0.0
github.com/sabhiram/go-gitignore v0.0.0-20201211210132-54b8a0bf510f
github.com/sourcegraph/go-diff v0.6.1
github.com/spf13/cobra v1.3.0
github.com/stretchr/testify v1.7.0
github.com/yosuke-furukawa/json5 v0.1.1
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20210423082822-04245dca01da // indirect
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect
golang.org/x/text v0.3.6 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)
732 changes: 717 additions & 15 deletions cli/go.sum

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion cli/internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ func (c *ApiClient) RecordAnalyticsEvents(events []map[string]interface{}) error
req.Header.Set("Authorization", "Bearer "+c.Token)
req.Header.Set("User-Agent", c.UserAgent())
resp, err := c.HttpClient.Do(req)
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
if resp != nil && resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
b, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("%s", string(b))
}
Expand Down
154 changes: 105 additions & 49 deletions cli/internal/login/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,29 @@ package login
import (
"context"
"fmt"
"net"
"net/http"
"net/url"
"os"
"os/signal"
"strings"

"github.com/pkg/errors"
"github.com/vercel/turborepo/cli/internal/client"
"github.com/vercel/turborepo/cli/internal/config"
"github.com/vercel/turborepo/cli/internal/ui"
"github.com/vercel/turborepo/cli/internal/util"
"github.com/vercel/turborepo/cli/internal/util/browser"

"github.com/fatih/color"
"github.com/hashicorp/go-hclog"
"github.com/mitchellh/cli"
"github.com/spf13/cobra"
)

// LoginCommand is a Command implementation allows the user to login to turbo
type LoginCommand struct {
Config *config.Config
Ui *cli.ColoredUi
UI *cli.ColoredUi
}

// Synopsis of run command
Expand All @@ -38,67 +43,118 @@ Usage: turbo login
return strings.TrimSpace(helpText)
}

const DEFAULT_HOSTNAME = "127.0.0.1"
const DEFAULT_PORT = 9789
const defaultHostname = "127.0.0.1"
const defaultPort = 9789

// Run logs into the api with PKCE and writes the token to turbo user config directory
func (c *LoginCommand) Run(args []string) int {
loginCommand := &cobra.Command{
Use: "turbo login",
Short: "Login to your Vercel account",
RunE: func(cmd *cobra.Command, args []string) error {
return run(c.Config, loginDeps{
ui: c.UI,
openURL: browser.OpenBrowser,
client: c.Config.ApiClient,
writeConfig: config.WriteUserConfigFile,
})
},
}
loginCommand.SetArgs(args)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:smart_meme:

err := loginCommand.Execute()
if err != nil {
c.Config.Logger.Error("error", err)
c.UI.Error(fmt.Sprintf("%s%s", ui.ERROR_PREFIX, color.RedString(" %v", err)))
return 1
}
return 0
}

type browserClient = func(url string) error
type userClient interface {
SetToken(token string)
GetUser() (*client.UserResponse, error)
}
type configWriter = func(cf *config.TurborepoConfig) error

type loginDeps struct {
ui *cli.ColoredUi
openURL browserClient
client userClient
writeConfig configWriter
}

func run(c *config.Config, deps loginDeps) error {
var rawToken string
c.Config.Logger.Debug(fmt.Sprintf("turbo v%v", c.Config.TurboVersion))
c.Config.Logger.Debug(fmt.Sprintf("api url: %v", c.Config.ApiUrl))
c.Config.Logger.Debug(fmt.Sprintf("login url: %v", c.Config.LoginUrl))
redirectUrl := fmt.Sprintf("http://%v:%v", DEFAULT_HOSTNAME, DEFAULT_PORT)
loginUrl := fmt.Sprintf("%v/turborepo/token?redirect_uri=%v", c.Config.LoginUrl, redirectUrl)
c.Ui.Info(util.Sprintf(">>> Opening browser to %v", c.Config.LoginUrl))
s := ui.NewSpinner(os.Stdout)
browser.OpenBrowser(loginUrl)
s.Start("Waiting for your authorization...")
c.Logger.Debug(fmt.Sprintf("turbo v%v", c.TurboVersion))
c.Logger.Debug(fmt.Sprintf("api url: %v", c.ApiUrl))
c.Logger.Debug(fmt.Sprintf("login url: %v", c.LoginUrl))
redirectURL := fmt.Sprintf("http://%v:%v", defaultHostname, defaultPort)
loginURL := fmt.Sprintf("%v/turborepo/token?redirect_uri=%v", c.LoginUrl, redirectURL)
deps.ui.Info(util.Sprintf(">>> Opening browser to %v", c.LoginUrl))

rootctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
// Start listening immediately to handle race with user interaction
// This is mostly for testing, but would otherwise still technically be
// a race condition.
addr := defaultHostname + ":" + fmt.Sprint(defaultPort)
l, err := net.Listen("tcp", addr)
if err != nil {
return err
}

redirectDone := make(chan struct{})
mux := http.NewServeMux()
var query url.Values
ctx, cancel := context.WithCancel(context.Background())
fmt.Println(query.Encode())
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
query = r.URL.Query()
http.Redirect(w, r, c.Config.LoginUrl+"/turborepo/success", http.StatusFound)
cancel()
http.Redirect(w, r, c.LoginUrl+"/turborepo/success", http.StatusFound)
close(redirectDone)
})

srv := &http.Server{Addr: DEFAULT_HOSTNAME + ":" + fmt.Sprint(DEFAULT_PORT)}
srv := &http.Server{Handler: mux}
var serverErr error
serverDone := make(chan struct{})
go func() {
if err := srv.ListenAndServe(); err != nil {
if err != nil {
c.logError(c.Config.Logger, "", fmt.Errorf("could not activate device. Please try again: %w", err))
}
if err := srv.Serve(l); err != nil {
serverErr = errors.Wrap(err, "could not activate device. Please try again")
}
close(serverDone)
}()
<-ctx.Done()
s.Stop("")
config.WriteUserConfigFile(&config.TurborepoConfig{Token: query.Get("token")})
rawToken = query.Get("token")
c.Config.ApiClient.SetToken(rawToken)
userResponse, err := c.Config.ApiClient.GetUser()

s := ui.NewSpinner(os.Stdout)
err = deps.openURL(loginURL)
if err != nil {
c.logError(c.Config.Logger, "", fmt.Errorf("could not get user information.\n: %w", err))
return 1
return errors.Wrapf(err, "failed to open %v", loginURL)
}
c.Ui.Info("")
c.Ui.Info(util.Sprintf("%s Turborepo CLI authorized for %s${RESET}", ui.Rainbow(">>> Success!"), userResponse.User.Email))
c.Ui.Info("")
c.Ui.Info(util.Sprintf("${CYAN}To connect to your Remote Cache. Run the following in the${RESET}"))
c.Ui.Info(util.Sprintf("${CYAN}root of any turborepo:${RESET}"))
c.Ui.Info("")
c.Ui.Info(util.Sprintf(" ${BOLD}npx turbo link${RESET}"))
c.Ui.Info("")
return 0
}

// logError logs an error and outputs it to the UI.
func (c *LoginCommand) logError(log hclog.Logger, prefix string, err error) {
log.Error(prefix, "error", err)
s.Start("Waiting for your authorization...")

if prefix != "" {
prefix += ": "
<-redirectDone
err = srv.Shutdown(rootctx)
// Stop the spinner before we return to ensure terminal is left in a good state
s.Stop("")
if err != nil {
return err
}

c.Ui.Error(fmt.Sprintf("%s%s%s", ui.ERROR_PREFIX, prefix, color.RedString(" %v", err)))
<-serverDone
if !errors.Is(serverErr, http.ErrServerClosed) {
return serverErr
}
deps.writeConfig(&config.TurborepoConfig{Token: query.Get("token")})
rawToken = query.Get("token")
deps.client.SetToken(rawToken)
userResponse, err := deps.client.GetUser()
if err != nil {
return errors.Wrap(err, "could not get user information")
}
deps.ui.Info("")
deps.ui.Info(util.Sprintf("%s Turborepo CLI authorized for %s${RESET}", ui.Rainbow(">>> Success!"), userResponse.User.Email))
deps.ui.Info("")
deps.ui.Info(util.Sprintf("${CYAN}To connect to your Remote Cache. Run the following in the${RESET}"))
deps.ui.Info(util.Sprintf("${CYAN}root of any turborepo:${RESET}"))
deps.ui.Info("")
deps.ui.Info(util.Sprintf(" ${BOLD}npx turbo link${RESET}"))
deps.ui.Info("")
return nil
}
93 changes: 93 additions & 0 deletions cli/internal/login/login_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package login

import (
"fmt"
"net/http"
"testing"

"github.com/hashicorp/go-hclog"
"github.com/vercel/turborepo/cli/internal/client"
"github.com/vercel/turborepo/cli/internal/config"
"github.com/vercel/turborepo/cli/internal/ui"
)

type dummyClient struct {
setToken string
}

func (d *dummyClient) SetToken(t string) {
d.setToken = t
}

func (d *dummyClient) GetUser() (*client.UserResponse, error) {
return &client.UserResponse{}, nil
}

func Test_run(t *testing.T) {
logger := hclog.Default()
cf := &config.Config{
Logger: logger,
TurboVersion: "test",
ApiUrl: "api-url",
LoginUrl: "login-url",
}

ch := make(chan struct{}, 1)
openedURL := ""
urlOpener := func(url string) error {
openedURL = url
ch <- struct{}{}
return nil
}

// When we get the ping, send a token
var clientErr error
go func() {
<-ch
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := client.Get("http://127.0.0.1:9789/?token=my-token")
if err != nil {
clientErr = err
} else if resp != nil && resp.StatusCode != http.StatusFound {
clientErr = fmt.Errorf("invalid status %v", resp.StatusCode)
}
ch <- struct{}{}
}()

var writtenConfig *config.TurborepoConfig
writeConfig := func(cf *config.TurborepoConfig) error {
writtenConfig = cf
return nil
}

client := &dummyClient{}
err := run(cf, loginDeps{
openURL: urlOpener,
ui: ui.Default(),
writeConfig: writeConfig,
client: client,
})
if err != nil {
t.Errorf("expected to succeed, got error %v", err)
}
<-ch
if clientErr != nil {
t.Errorf("test client had error %v", clientErr)
}

expectedURL := "login-url/turborepo/token?redirect_uri=http://127.0.0.1:9789"
if openedURL != expectedURL {
t.Errorf("openedURL got %v, want %v", openedURL, expectedURL)
}

if writtenConfig.Token != "my-token" {
t.Errorf("config token got %v, want my-token", writtenConfig.Token)
}
if client.setToken != "my-token" {
t.Errorf("user client token got %v, want my-token", client.setToken)
}
}
Loading