diff --git a/auth/auth_ui/cli.go b/auth/auth_ui/cli.go index db8d1f75..b5b79799 100644 --- a/auth/auth_ui/cli.go +++ b/auth/auth_ui/cli.go @@ -109,3 +109,9 @@ func prompt(w io.Writer, prompt string, readlnFn func(*os.File) (string, error)) fmt.Fprintln(w, "input cannot be empty") } } + +func (*CLI) ConfirmationCode(email string) (code int, err error) { + fmt.Printf("Enter confirmation code sent to %s: ", email) + _, err = fmt.Fscanf(os.Stdin, "%d", &code) + return +} diff --git a/auth/auth_ui/huh.go b/auth/auth_ui/huh.go index cef4c9b5..f286ead4 100644 --- a/auth/auth_ui/huh.go +++ b/auth/auth_ui/huh.go @@ -4,6 +4,8 @@ import ( "errors" "fmt" "io" + "regexp" + "strconv" "github.com/charmbracelet/huh" ) @@ -102,3 +104,30 @@ func valSepEaster() func(v int) error { return nil } } + +func (*Huh) ConfirmationCode(email string) (int, error) { + var strCode string + q := huh.NewInput(). + CharLimit(6). + Title(fmt.Sprintf("Enter confirmation code sent to %s", email)). + Description("Slack did not recognise the browser, and sent a confirmation code. Please enter the confirmation code below."). + Value(&strCode). + Validate(valSixDigits) + if err := q.Run(); err != nil { + return 0, err + } + code, err := strconv.Atoi(strCode) + if err != nil { + return 0, err + } + return code, nil +} + +var numChlgRE = regexp.MustCompile(`^\d{6}$`) + +func valSixDigits(s string) error { + if numChlgRE.MatchString(s) { + return nil + } + return errors.New("confirmation code must be a sequence of six digits") +} diff --git a/auth/auth_ui/huh_test.go b/auth/auth_ui/huh_test.go new file mode 100644 index 00000000..4e802a93 --- /dev/null +++ b/auth/auth_ui/huh_test.go @@ -0,0 +1,47 @@ +package auth_ui + +import "testing" + +func Test_valSixDigits(t *testing.T) { + type args struct { + s string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + "empty", + args{""}, + true, + }, + { + "too short", + args{"12345"}, + true, + }, + { + "too long", + args{"1234567"}, + true, + }, + { + "not a number", + args{"123456a"}, + true, + }, + { + "valid", + args{"123456"}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := valSixDigits(tt.args.s); (err != nil) != tt.wantErr { + t.Errorf("valSixDigits() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/auth/rod.go b/auth/rod.go index f313f200..31e7c255 100644 --- a/auth/rod.go +++ b/auth/rod.go @@ -5,9 +5,7 @@ import ( "fmt" "io" "os" - "time" - "github.com/charmbracelet/huh/spinner" "github.com/rusq/slackauth" "github.com/rusq/slackdump/v2/auth/auth_ui" ) @@ -29,10 +27,9 @@ type browserAuthUIExt interface { BrowserAuthUI RequestLoginType(w io.Writer) (int, error) RequestCreds(w io.Writer, workspace string) (email string, passwd string, err error) + ConfirmationCode(email string) (code int, err error) } -const expectedLoginDuration = 16 * time.Second - func NewRODAuth(ctx context.Context, opts ...Option) (RodAuth, error) { r := RodAuth{ opts: options{ @@ -74,26 +71,10 @@ func NewRODAuth(ctx context.Context, opts ...Option) (RodAuth, error) { return r, err } case auth_ui.LoginEmail: - username, password, err := r.opts.ui.RequestCreds(os.Stdout, r.opts.workspace) + sp, err = headlessFlow(ctx, r.opts.workspace, r.opts.ui) if err != nil { return r, err } - if username == "" { - return r, fmt.Errorf("email cannot be empty") - } - if password == "" { - return r, fmt.Errorf("password cannot be empty") - } - var loginErr error - spin := spinner.New().Title("Logging in...").Action(func() { - sp.Token, sp.Cookie, loginErr = slackauth.Headless(ctx, r.opts.workspace, username, password) - }) - if err := spin.Run(); err != nil { - return r, err - } - if loginErr != nil { - return r, loginErr - } fmt.Fprintln(os.Stderr, "authenticated.") case auth_ui.LoginCancel: return r, ErrCancelled @@ -103,3 +84,32 @@ func NewRODAuth(ctx context.Context, opts ...Option) (RodAuth, error) { simpleProvider: sp, }, nil } + +func headlessFlow(ctx context.Context, workspace string, ui browserAuthUIExt) (sp simpleProvider, err error) { + username, password, err := ui.RequestCreds(os.Stdout, workspace) + if err != nil { + return sp, err + } + if username == "" { + return sp, fmt.Errorf("email cannot be empty") + } + if password == "" { + return sp, fmt.Errorf("password cannot be empty") + } + fmt.Println("Logging in to Slack, depending on your connection speed, it will take 15-30 seconds...") + + var loginErr error + sp.Token, sp.Cookie, loginErr = slackauth.Headless( + ctx, + workspace, + username, + password, + slackauth.WithChallengeFunc(ui.ConfirmationCode), + ) + if loginErr != nil { + return sp, loginErr + } + + fmt.Fprintln(os.Stderr, "authenticated.") + return +} diff --git a/go.mod b/go.mod index f680503a..bfee6bdd 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/rusq/dlog v1.4.0 github.com/rusq/osenv/v2 v2.0.1 github.com/rusq/secure v0.0.4 - github.com/rusq/slackauth v0.0.5 + github.com/rusq/slackauth v0.0.6 github.com/rusq/tracer v1.0.1 github.com/schollz/progressbar/v3 v3.13.0 github.com/slack-go/slack v0.12.1 diff --git a/go.sum b/go.sum index 01768987..5adf8713 100644 --- a/go.sum +++ b/go.sum @@ -117,6 +117,8 @@ github.com/rusq/slackauth v0.0.4 h1:AMR0bRTHM7Kg2N14bPWm8bG+J5hXOoLrgiGUmFaBsMU= github.com/rusq/slackauth v0.0.4/go.mod h1:zb1PJY2+8uEqn0RiWuRjnd+ZFwwfnvA5xrGoooVUgNY= github.com/rusq/slackauth v0.0.5 h1:qhRkhLa+tS60OGPhzlO/mXbQiWxnzPkoVixqy18n7eI= github.com/rusq/slackauth v0.0.5/go.mod h1:zb1PJY2+8uEqn0RiWuRjnd+ZFwwfnvA5xrGoooVUgNY= +github.com/rusq/slackauth v0.0.6 h1:vV4kg3lRKV+oiHVAWxyKXa9aoRU4XwT5pSQ0mlo9OSM= +github.com/rusq/slackauth v0.0.6/go.mod h1:zb1PJY2+8uEqn0RiWuRjnd+ZFwwfnvA5xrGoooVUgNY= github.com/rusq/tracer v1.0.1 h1:5u4PCV8NGO97VuAINQA4gOVRkPoqHimLE2jpezRVNMU= github.com/rusq/tracer v1.0.1/go.mod h1:Rqu48C3/K8bA5NPmF20Hft73v431MQIdM+Co+113pME= github.com/schollz/progressbar/v3 v3.13.0 h1:9TeeWRcjW2qd05I8Kf9knPkW4vLM/hYoa6z9ABvxje8=