diff --git a/auth/auth_ui/auth_ui.go b/auth/auth_ui/auth_ui.go index 1c1b445c..b978124e 100644 --- a/auth/auth_ui/auth_ui.go +++ b/auth/auth_ui/auth_ui.go @@ -5,12 +5,20 @@ import ( "strings" ) +// LoginType is the login type, that is used to choose the authentication flow, +// for example login headlessly or interactively. +type LoginType int8 + const ( - LoginEmail = 0 - LoginSSO = 1 - LoginCancel = 2 + // LHeadless is the email/password login type. + LHeadless LoginType = iota + // LInteractive is the SSO login type (Google, Apple, etc). + LInteractive + // LCancel should be returned if the user cancels the login intent. + LCancel ) +// Sanitize takes a workspace name or URL and returns the workspace name. func Sanitize(workspace string) (string, error) { if !strings.Contains(workspace, ".slack.com") { return workspace, nil diff --git a/auth/auth_ui/cli.go b/auth/auth_ui/cli.go index b5b79799..fb60affb 100644 --- a/auth/auth_ui/cli.go +++ b/auth/auth_ui/cli.go @@ -42,17 +42,17 @@ func (cl *CLI) RequestPassword(w io.Writer, account string) (string, error) { return prompt(w, fmt.Sprintf("Enter Password for %s (won't be visible): ", account), readpwd) } -func (cl *CLI) RequestLoginType(w io.Writer) (int, error) { +func (cl *CLI) RequestLoginType(w io.Writer) (LoginType, error) { var types = []struct { name string - value int + value LoginType }{ - {"Email", LoginEmail}, - {"Google", LoginSSO}, - {"Apple", LoginSSO}, - {"Login with Single-Sign-On (SSO)", LoginSSO}, - {"Other/Manual", LoginSSO}, - {"Cancel", LoginCancel}, + {"Email", LHeadless}, + {"Google", LInteractive}, + {"Apple", LInteractive}, + {"Login with Single-Sign-On (SSO)", LInteractive}, + {"Other/Manual", LInteractive}, + {"Cancel", LCancel}, } var idx int = -1 diff --git a/auth/auth_ui/huh.go b/auth/auth_ui/huh.go index f286ead4..63f26606 100644 --- a/auth/auth_ui/huh.go +++ b/auth/auth_ui/huh.go @@ -10,6 +10,7 @@ import ( "github.com/charmbracelet/huh" ) +// Huh is the Auth UI that uses the huh library to provide a terminal UI. type Huh struct { theme huh.Theme } @@ -46,17 +47,17 @@ func (*Huh) RequestCreds(w io.Writer, workspace string) (email string, passwd st return } -func (*Huh) RequestLoginType(w io.Writer) (int, error) { - var loginType int - err := huh.NewSelect[int]().Title("Select login type"). +func (*Huh) RequestLoginType(w io.Writer) (LoginType, error) { + var loginType LoginType + err := huh.NewSelect[LoginType]().Title("Select login type"). Options( - huh.NewOption("Email", LoginEmail), - huh.NewOption("Google", LoginSSO), - huh.NewOption("Apple", LoginSSO), - huh.NewOption("Login with Single-Sign-On (SSO)", LoginSSO), - huh.NewOption("Other/Manual", LoginSSO), - huh.NewOption("------", -1), - huh.NewOption("Cancel", LoginCancel), + huh.NewOption("Email", LHeadless), + huh.NewOption("Google", LInteractive), + huh.NewOption("Apple", LInteractive), + huh.NewOption("Login with Single-Sign-On (SSO)", LInteractive), + huh.NewOption("Other/Manual", LInteractive), + huh.NewOption("------", LoginType(-1)), + huh.NewOption("Cancel", LCancel), ). Value(&loginType). Validate(valSepEaster()). @@ -65,46 +66,8 @@ func (*Huh) RequestLoginType(w io.Writer) (int, error) { return loginType, err } -func valSepEaster() func(v int) error { - var phrases = []string{ - "This is a separator, it does nothing", - "Seriously, it does nothing", - "Stop clicking on it", - "Stop it", - "Stop", - "Why are you so persistent?", - "Fine, you win", - "Here's a cookie: 🍪", - "🍪", - "🍪", - "Don't be greedy, you already had three.", - "Ok, here's another one: 🍪", - "Nothing will happen if you click on it again", - "", - "", - "", - "You must have a lot of time on your hands", - "Or maybe you're just bored", - "Or maybe you're just procrastinating", - "Or maybe you're just trying to get a cookie", - "These are virtual cookies, you can't eat them, but here's another one: 🍪", - "🍪", - "You have reached the end of this joke, it will now repeat", - "Seriously...", - "Ah, shit, here we go again", - } - var i int - return func(v int) error { - if v == -1 { - // separator selected - msg := phrases[i] - i = (i + 1) % len(phrases) - return errors.New(msg) - } - return nil - } -} - +// ConfirmationCode asks the user to input the confirmation code, does some +// validation on it and returns it as an int. func (*Huh) ConfirmationCode(email string) (int, error) { var strCode string q := huh.NewInput(). diff --git a/auth/auth_ui/validation.go b/auth/auth_ui/validation.go index f5792c50..01d1c88e 100644 --- a/auth/auth_ui/validation.go +++ b/auth/auth_ui/validation.go @@ -59,3 +59,44 @@ func valEmail(s string) error { } return nil } + +// valSepEaster is probably the most useless validation function ever. +func valSepEaster() func(v LoginType) error { + var phrases = []string{ + "This is a separator, it does nothing", + "Seriously, it does nothing", + "Stop clicking on it", + "Stop it", + "Stop", + "Why are you so persistent?", + "Fine, you win", + "Here's a cookie: 🍪", + "🍪", + "🍪", + "Don't be greedy, you already had three.", + "Ok, here's another one: 🍪", + "Nothing will happen if you click on it again", + "", + "", + "", + "You must have a lot of time on your hands", + "Or maybe you're just bored", + "Or maybe you're just procrastinating", + "Or maybe you're just trying to get a cookie", + "These are virtual cookies, you can't eat them, but here's another one: 🍪", + "🍪", + "You have reached the end of this joke, it will now repeat", + "Seriously...", + "Ah, shit, here we go again", + } + var i int + return func(v LoginType) error { + if v == -1 { + // separator selected + msg := phrases[i] + i = (i + 1) % len(phrases) + return errors.New(msg) + } + return nil + } +} diff --git a/auth/rod.go b/auth/rod.go index 31e7c255..da7afa9e 100644 --- a/auth/rod.go +++ b/auth/rod.go @@ -10,6 +10,17 @@ import ( "github.com/rusq/slackdump/v2/auth/auth_ui" ) +// RodAuth is an authentication provider that uses a headless or interactive +// browser to authenticate with Slack, depending on the user's choice. It uses +// rod library to drive the browser via the CDP protocol. +// +// User can choose between: +// - Email/password login - will be done headlessly +// - SSO authentication - will open the browser and let the user do the thing. +// - Cancel - will cancel the login flow. +// +// Headless login is a bit fragile. If it fails, user should be advised to +// login interactively by choosing SSO auth type. type RodAuth struct { simpleProvider opts options @@ -25,11 +36,19 @@ type rodOpts struct { type browserAuthUIExt interface { BrowserAuthUI - RequestLoginType(w io.Writer) (int, error) + // RequestLoginType should request the login type from the user and return + // one of the [auth_ui.LoginType] constants. The implementation should + // provide a way to cancel the login flow, returning [auth_ui.LoginCancel]. + RequestLoginType(w io.Writer) (auth_ui.LoginType, error) + // RequestCreds should request the user's email and password and return + // them. RequestCreds(w io.Writer, workspace string) (email string, passwd string, err error) + // ConfirmationCode should request the confirmation code from the user and + // return it. ConfirmationCode(email string) (code int, err error) } +// NewRODAuth constructs new RodAuth provider. func NewRODAuth(ctx context.Context, opts ...Option) (RodAuth, error) { r := RodAuth{ opts: options{ @@ -64,19 +83,19 @@ func NewRODAuth(ctx context.Context, opts ...Option) (RodAuth, error) { var sp simpleProvider switch resp { - case auth_ui.LoginSSO: + case auth_ui.LInteractive: var err error sp.Token, sp.Cookie, err = slackauth.Browser(ctx, r.opts.workspace) if err != nil { return r, err } - case auth_ui.LoginEmail: + case auth_ui.LHeadless: sp, err = headlessFlow(ctx, r.opts.workspace, r.opts.ui) if err != nil { return r, err } fmt.Fprintln(os.Stderr, "authenticated.") - case auth_ui.LoginCancel: + case auth_ui.LCancel: return r, ErrCancelled } diff --git a/go.mod b/go.mod index b92dac33..d5285a33 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/rusq/encio v0.1.0 github.com/rusq/fsadapter v1.0.1 github.com/rusq/osenv/v2 v2.0.1 - github.com/rusq/slackauth v0.0.3 + 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.3 diff --git a/go.sum b/go.sum index 2933525b..3f53df8a 100644 --- a/go.sum +++ b/go.sum @@ -144,6 +144,8 @@ github.com/rusq/slackauth v0.0.1 h1:UYW/lMr+FMnKeax40L55VpY40Ekmsoc0MhScdL4fopc= github.com/rusq/slackauth v0.0.1/go.mod h1:zb1PJY2+8uEqn0RiWuRjnd+ZFwwfnvA5xrGoooVUgNY= github.com/rusq/slackauth v0.0.3 h1:cpq1xqDPm9Fkng/yrQJcugTLdO7f1GyJLn/mA7/Cx9Q= github.com/rusq/slackauth v0.0.3/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=