From 2f8fa39a9bfeb487e352ff83ecb5391b8a127326 Mon Sep 17 00:00:00 2001 From: Stavros Date: Mon, 6 Oct 2025 21:27:51 +0300 Subject: [PATCH] refactor: make cli modular (#390) * refactor: make cli modular * chore: apply suggestion from @Rycochet Co-authored-by: Ryc O'Chet * chore: apply review suggestions * refactor: no need to handle user escaping in verify cmd --------- Co-authored-by: Ryc O'Chet --- cmd/create.go | 99 +++++++++++++++++++++++++ cmd/generate.go | 120 ++++++++++++++++++++++++++++++ cmd/root.go | 133 +++++++++++++++++++++------------- cmd/totp/generate/generate.go | 99 ------------------------- cmd/totp/totp.go | 17 ----- cmd/user/create/create.go | 80 -------------------- cmd/user/user.go | 19 ----- cmd/user/verify/verify.go | 101 -------------------------- cmd/verify.go | 118 ++++++++++++++++++++++++++++++ cmd/version.go | 41 ++++++++--- main.go | 2 +- 11 files changed, 452 insertions(+), 377 deletions(-) create mode 100644 cmd/create.go create mode 100644 cmd/generate.go delete mode 100644 cmd/totp/generate/generate.go delete mode 100644 cmd/totp/totp.go delete mode 100644 cmd/user/create/create.go delete mode 100644 cmd/user/user.go delete mode 100644 cmd/user/verify/verify.go create mode 100644 cmd/verify.go diff --git a/cmd/create.go b/cmd/create.go new file mode 100644 index 0000000..0abb3c7 --- /dev/null +++ b/cmd/create.go @@ -0,0 +1,99 @@ +package cmd + +import ( + "errors" + "fmt" + "strings" + + "github.com/charmbracelet/huh" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + "golang.org/x/crypto/bcrypt" +) + +type createUserCmd struct { + root *cobra.Command + cmd *cobra.Command + + interactive bool + docker bool + username string + password string +} + +func newCreateUserCmd(root *cobra.Command) *createUserCmd { + return &createUserCmd{ + root: root, + } +} + +func (c *createUserCmd) Register() { + c.cmd = &cobra.Command{ + Use: "create", + Short: "Create a user", + Long: `Create a user either interactively or by passing flags.`, + Run: c.run, + } + + c.cmd.Flags().BoolVarP(&c.interactive, "interactive", "i", false, "Create a user interactively") + c.cmd.Flags().BoolVar(&c.docker, "docker", false, "Format output for docker") + c.cmd.Flags().StringVar(&c.username, "username", "", "Username") + c.cmd.Flags().StringVar(&c.password, "password", "", "Password") + + if c.root != nil { + c.root.AddCommand(c.cmd) + } +} + +func (c *createUserCmd) GetCmd() *cobra.Command { + return c.cmd +} + +func (c *createUserCmd) run(cmd *cobra.Command, args []string) { + log.Logger = log.Level(zerolog.InfoLevel) + + if c.interactive { + form := huh.NewForm( + huh.NewGroup( + huh.NewInput().Title("Username").Value(&c.username).Validate((func(s string) error { + if s == "" { + return errors.New("username cannot be empty") + } + return nil + })), + huh.NewInput().Title("Password").Value(&c.password).Validate((func(s string) error { + if s == "" { + return errors.New("password cannot be empty") + } + return nil + })), + huh.NewSelect[bool]().Title("Format the output for Docker?").Options(huh.NewOption("Yes", true), huh.NewOption("No", false)).Value(&c.docker), + ), + ) + var baseTheme *huh.Theme = huh.ThemeBase() + err := form.WithTheme(baseTheme).Run() + if err != nil { + log.Fatal().Err(err).Msg("Form failed") + } + } + + if c.username == "" || c.password == "" { + log.Fatal().Err(errors.New("error invalid input")).Msg("Username and password cannot be empty") + } + + log.Info().Str("username", c.username).Msg("Creating user") + + passwd, err := bcrypt.GenerateFromPassword([]byte(c.password), bcrypt.DefaultCost) + if err != nil { + log.Fatal().Err(err).Msg("Failed to hash password") + } + + // If docker format is enabled, escape the dollar sign + passwdStr := string(passwd) + if c.docker { + passwdStr = strings.ReplaceAll(passwdStr, "$", "$$") + } + + log.Info().Str("user", fmt.Sprintf("%s:%s", c.username, passwdStr)).Msg("User created") +} diff --git a/cmd/generate.go b/cmd/generate.go new file mode 100644 index 0000000..005b473 --- /dev/null +++ b/cmd/generate.go @@ -0,0 +1,120 @@ +package cmd + +import ( + "errors" + "fmt" + "os" + "strings" + "tinyauth/internal/utils" + + "github.com/charmbracelet/huh" + "github.com/mdp/qrterminal/v3" + "github.com/pquerna/otp/totp" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +type generateTotpCmd struct { + root *cobra.Command + cmd *cobra.Command + + interactive bool + user string +} + +func newGenerateTotpCmd(root *cobra.Command) *generateTotpCmd { + return &generateTotpCmd{ + root: root, + } +} + +func (c *generateTotpCmd) Register() { + c.cmd = &cobra.Command{ + Use: "generate", + Short: "Generate a totp secret", + Long: `Generate a totp secret for a user either interactively or by passing flags.`, + Run: c.run, + } + + c.cmd.Flags().BoolVarP(&c.interactive, "interactive", "i", false, "Run in interactive mode") + c.cmd.Flags().StringVar(&c.user, "user", "", "Your current user (username:hash)") + + if c.root != nil { + c.root.AddCommand(c.cmd) + } +} + +func (c *generateTotpCmd) GetCmd() *cobra.Command { + return c.cmd +} + +func (c *generateTotpCmd) run(cmd *cobra.Command, args []string) { + log.Logger = log.Level(zerolog.InfoLevel) + + if c.interactive { + form := huh.NewForm( + huh.NewGroup( + huh.NewInput().Title("Current user (username:hash)").Value(&c.user).Validate((func(s string) error { + if s == "" { + return errors.New("user cannot be empty") + } + return nil + })), + ), + ) + var baseTheme *huh.Theme = huh.ThemeBase() + err := form.WithTheme(baseTheme).Run() + if err != nil { + log.Fatal().Err(err).Msg("Form failed") + } + } + + user, err := utils.ParseUser(c.user) + if err != nil { + log.Fatal().Err(err).Msg("Failed to parse user") + } + + docker := false + if strings.Contains(c.user, "$$") { + docker = true + } + + if user.TotpSecret != "" { + log.Fatal().Msg("User already has a TOTP secret") + } + + key, err := totp.Generate(totp.GenerateOpts{ + Issuer: "Tinyauth", + AccountName: user.Username, + }) + + if err != nil { + log.Fatal().Err(err).Msg("Failed to generate TOTP secret") + } + + secret := key.Secret() + + log.Info().Str("secret", secret).Msg("Generated TOTP secret") + + log.Info().Msg("Generated QR code") + + config := qrterminal.Config{ + Level: qrterminal.L, + Writer: os.Stdout, + BlackChar: qrterminal.BLACK, + WhiteChar: qrterminal.WHITE, + QuietZone: 2, + } + + qrterminal.GenerateWithConfig(key.URL(), config) + + user.TotpSecret = secret + + // If using docker escape re-escape it + if docker { + user.Password = strings.ReplaceAll(user.Password, "$", "$$") + } + + log.Info().Str("user", fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TotpSecret)).Msg("Add the totp secret to your authenticator app then use the verify command to ensure everything is working correctly.") +} diff --git a/cmd/root.go b/cmd/root.go index 723cb36..e7bbb13 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,8 +2,6 @@ package cmd import ( "strings" - totpCmd "tinyauth/cmd/totp" - userCmd "tinyauth/cmd/user" "tinyauth/internal/bootstrap" "tinyauth/internal/config" "tinyauth/internal/utils" @@ -15,55 +13,28 @@ import ( "github.com/spf13/viper" ) -var rootCmd = &cobra.Command{ - Use: "tinyauth", - Short: "The simplest way to protect your apps with a login screen.", - Long: `Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic OAuth provider to all of your docker apps.`, - Run: func(cmd *cobra.Command, args []string) { - var conf config.Config +type rootCmd struct { + root *cobra.Command + cmd *cobra.Command - err := viper.Unmarshal(&conf) - if err != nil { - log.Fatal().Err(err).Msg("Failed to parse config") - } - - // Validate config - v := validator.New() - - err = v.Struct(conf) - if err != nil { - log.Fatal().Err(err).Msg("Invalid config") - } - - log.Logger = log.Level(zerolog.Level(utils.GetLogLevel(conf.LogLevel))) - log.Info().Str("version", strings.TrimSpace(config.Version)).Msg("Starting tinyauth") - - // Create bootstrap app - app := bootstrap.NewBootstrapApp(conf) - - // Run - err = app.Setup() - - if err != nil { - log.Fatal().Err(err).Msg("Failed to setup app") - } - - }, + viper *viper.Viper } -func Execute() { - rootCmd.FParseErrWhitelist.UnknownFlags = true - err := rootCmd.Execute() - if err != nil { - log.Fatal().Err(err).Msg("Failed to execute command") +func newRootCmd() *rootCmd { + return &rootCmd{ + viper: viper.New(), } } -func init() { - rootCmd.AddCommand(userCmd.UserCmd()) - rootCmd.AddCommand(totpCmd.TotpCmd()) +func (c *rootCmd) Register() { + c.cmd = &cobra.Command{ + Use: "tinyauth", + Short: "The simplest way to protect your apps with a login screen", + Long: `Tinyauth is a simple authentication middleware that adds a simple login screen or OAuth with Google, Github or any other provider to all of your docker apps.`, + Run: c.run, + } - viper.AutomaticEnv() + c.viper.AutomaticEnv() configOptions := []struct { name string @@ -101,17 +72,81 @@ func init() { for _, opt := range configOptions { switch v := opt.defaultVal.(type) { case bool: - rootCmd.Flags().Bool(opt.name, v, opt.description) + c.cmd.Flags().Bool(opt.name, v, opt.description) case int: - rootCmd.Flags().Int(opt.name, v, opt.description) + c.cmd.Flags().Int(opt.name, v, opt.description) case string: - rootCmd.Flags().String(opt.name, v, opt.description) + c.cmd.Flags().String(opt.name, v, opt.description) } // Create uppercase env var name envVar := strings.ReplaceAll(strings.ToUpper(opt.name), "-", "_") - viper.BindEnv(opt.name, envVar) + c.viper.BindEnv(opt.name, envVar) } - viper.BindPFlags(rootCmd.Flags()) + c.viper.BindPFlags(c.cmd.Flags()) + + if c.root != nil { + c.root.AddCommand(c.cmd) + } +} + +func (c *rootCmd) GetCmd() *cobra.Command { + return c.cmd +} + +func (c *rootCmd) run(cmd *cobra.Command, args []string) { + var conf config.Config + + err := c.viper.Unmarshal(&conf) + if err != nil { + log.Fatal().Err(err).Msg("Failed to parse config") + } + + v := validator.New() + err = v.Struct(conf) + if err != nil { + log.Fatal().Err(err).Msg("Invalid config") + } + + log.Logger = log.Level(zerolog.Level(utils.GetLogLevel(conf.LogLevel))) + log.Info().Str("version", strings.TrimSpace(config.Version)).Msg("Starting Tinyauth") + + app := bootstrap.NewBootstrapApp(conf) + + err = app.Setup() + if err != nil { + log.Fatal().Err(err).Msg("Failed to setup app") + } +} + +func Run() { + rootCmd := newRootCmd() + rootCmd.Register() + root := rootCmd.GetCmd() + + userCmd := &cobra.Command{ + Use: "user", + Short: "User utilities", + Long: `Utilities for creating and verifying tinyauth compatible users.`, + } + totpCmd := &cobra.Command{ + Use: "totp", + Short: "Totp utilities", + Long: `Utilities for creating and verifying totp codes.`, + } + + newCreateUserCmd(userCmd).Register() + newVerifyUserCmd(userCmd).Register() + newGenerateTotpCmd(totpCmd).Register() + newVersionCmd(root).Register() + + root.AddCommand(userCmd) + root.AddCommand(totpCmd) + + err := root.Execute() + + if err != nil { + log.Fatal().Err(err).Msg("Failed to execute root command") + } } diff --git a/cmd/totp/generate/generate.go b/cmd/totp/generate/generate.go deleted file mode 100644 index 72f0c29..0000000 --- a/cmd/totp/generate/generate.go +++ /dev/null @@ -1,99 +0,0 @@ -package generate - -import ( - "errors" - "fmt" - "os" - "strings" - "tinyauth/internal/utils" - - "github.com/charmbracelet/huh" - "github.com/mdp/qrterminal/v3" - "github.com/pquerna/otp/totp" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" -) - -var interactive bool - -// Input user -var iUser string - -var GenerateCmd = &cobra.Command{ - Use: "generate", - Short: "Generate a totp secret", - Run: func(cmd *cobra.Command, args []string) { - log.Logger = log.Level(zerolog.InfoLevel) - - if interactive { - form := huh.NewForm( - huh.NewGroup( - huh.NewInput().Title("Current username:hash").Value(&iUser).Validate((func(s string) error { - if s == "" { - return errors.New("user cannot be empty") - } - return nil - })), - ), - ) - var baseTheme *huh.Theme = huh.ThemeBase() - err := form.WithTheme(baseTheme).Run() - if err != nil { - log.Fatal().Err(err).Msg("Form failed") - } - } - - user, err := utils.ParseUser(iUser) - if err != nil { - log.Fatal().Err(err).Msg("Failed to parse user") - } - - dockerEscape := false - if strings.Contains(iUser, "$$") { - dockerEscape = true - } - - if user.TotpSecret != "" { - log.Fatal().Msg("User already has a totp secret") - } - - key, err := totp.Generate(totp.GenerateOpts{ - Issuer: "Tinyauth", - AccountName: user.Username, - }) - if err != nil { - log.Fatal().Err(err).Msg("Failed to generate totp secret") - } - - secret := key.Secret() - - log.Info().Str("secret", secret).Msg("Generated totp secret") - - log.Info().Msg("Generated QR code") - - config := qrterminal.Config{ - Level: qrterminal.L, - Writer: os.Stdout, - BlackChar: qrterminal.BLACK, - WhiteChar: qrterminal.WHITE, - QuietZone: 2, - } - - qrterminal.GenerateWithConfig(key.URL(), config) - - user.TotpSecret = secret - - // If using docker escape re-escape it - if dockerEscape { - user.Password = strings.ReplaceAll(user.Password, "$", "$$") - } - - log.Info().Str("user", fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TotpSecret)).Msg("Add the totp secret to your authenticator app then use the verify command to ensure everything is working correctly.") - }, -} - -func init() { - GenerateCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Run in interactive mode") - GenerateCmd.Flags().StringVar(&iUser, "user", "", "Your current username:hash") -} diff --git a/cmd/totp/totp.go b/cmd/totp/totp.go deleted file mode 100644 index bfe08aa..0000000 --- a/cmd/totp/totp.go +++ /dev/null @@ -1,17 +0,0 @@ -package cmd - -import ( - "tinyauth/cmd/totp/generate" - - "github.com/spf13/cobra" -) - -func TotpCmd() *cobra.Command { - totpCmd := &cobra.Command{ - Use: "totp", - Short: "Totp utilities", - Long: `Utilities for creating and verifying totp codes.`, - } - totpCmd.AddCommand(generate.GenerateCmd) - return totpCmd -} diff --git a/cmd/user/create/create.go b/cmd/user/create/create.go deleted file mode 100644 index ca5f95e..0000000 --- a/cmd/user/create/create.go +++ /dev/null @@ -1,80 +0,0 @@ -package create - -import ( - "errors" - "fmt" - "strings" - - "github.com/charmbracelet/huh" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - "golang.org/x/crypto/bcrypt" -) - -var interactive bool -var docker bool - -// i stands for input -var iUsername string -var iPassword string - -var CreateCmd = &cobra.Command{ - Use: "create", - Short: "Create a user", - Long: `Create a user either interactively or by passing flags.`, - Run: func(cmd *cobra.Command, args []string) { - log.Logger = log.Level(zerolog.InfoLevel) - - if interactive { - form := huh.NewForm( - huh.NewGroup( - huh.NewInput().Title("Username").Value(&iUsername).Validate((func(s string) error { - if s == "" { - return errors.New("username cannot be empty") - } - return nil - })), - huh.NewInput().Title("Password").Value(&iPassword).Validate((func(s string) error { - if s == "" { - return errors.New("password cannot be empty") - } - return nil - })), - huh.NewSelect[bool]().Title("Format the output for docker?").Options(huh.NewOption("Yes", true), huh.NewOption("No", false)).Value(&docker), - ), - ) - var baseTheme *huh.Theme = huh.ThemeBase() - err := form.WithTheme(baseTheme).Run() - if err != nil { - log.Fatal().Err(err).Msg("Form failed") - } - } - - if iUsername == "" || iPassword == "" { - log.Fatal().Err(errors.New("error invalid input")).Msg("Username and password cannot be empty") - } - - log.Info().Str("username", iUsername).Str("password", iPassword).Bool("docker", docker).Msg("Creating user") - - password, err := bcrypt.GenerateFromPassword([]byte(iPassword), bcrypt.DefaultCost) - if err != nil { - log.Fatal().Err(err).Msg("Failed to hash password") - } - - // If docker format is enabled, escape the dollar sign - passwordString := string(password) - if docker { - passwordString = strings.ReplaceAll(passwordString, "$", "$$") - } - - log.Info().Str("user", fmt.Sprintf("%s:%s", iUsername, passwordString)).Msg("User created") - }, -} - -func init() { - CreateCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Create a user interactively") - CreateCmd.Flags().BoolVar(&docker, "docker", false, "Format output for docker") - CreateCmd.Flags().StringVar(&iUsername, "username", "", "Username") - CreateCmd.Flags().StringVar(&iPassword, "password", "", "Password") -} diff --git a/cmd/user/user.go b/cmd/user/user.go deleted file mode 100644 index ce7f423..0000000 --- a/cmd/user/user.go +++ /dev/null @@ -1,19 +0,0 @@ -package cmd - -import ( - "tinyauth/cmd/user/create" - "tinyauth/cmd/user/verify" - - "github.com/spf13/cobra" -) - -func UserCmd() *cobra.Command { - userCmd := &cobra.Command{ - Use: "user", - Short: "User utilities", - Long: `Utilities for creating and verifying tinyauth compatible users.`, - } - userCmd.AddCommand(create.CreateCmd) - userCmd.AddCommand(verify.VerifyCmd) - return userCmd -} diff --git a/cmd/user/verify/verify.go b/cmd/user/verify/verify.go deleted file mode 100644 index 4bff6ed..0000000 --- a/cmd/user/verify/verify.go +++ /dev/null @@ -1,101 +0,0 @@ -package verify - -import ( - "errors" - "tinyauth/internal/utils" - - "github.com/charmbracelet/huh" - "github.com/pquerna/otp/totp" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - "golang.org/x/crypto/bcrypt" -) - -var interactive bool -var docker bool - -// i stands for input -var iUsername string -var iPassword string -var iTotp string -var iUser string - -var VerifyCmd = &cobra.Command{ - Use: "verify", - Short: "Verify a user is set up correctly", - Long: `Verify a user is set up correctly meaning that it has a correct username, password and totp code.`, - Run: func(cmd *cobra.Command, args []string) { - log.Logger = log.Level(zerolog.InfoLevel) - - if interactive { - form := huh.NewForm( - huh.NewGroup( - huh.NewInput().Title("User (username:hash:totp)").Value(&iUser).Validate((func(s string) error { - if s == "" { - return errors.New("user cannot be empty") - } - return nil - })), - huh.NewInput().Title("Username").Value(&iUsername).Validate((func(s string) error { - if s == "" { - return errors.New("username cannot be empty") - } - return nil - })), - huh.NewInput().Title("Password").Value(&iPassword).Validate((func(s string) error { - if s == "" { - return errors.New("password cannot be empty") - } - return nil - })), - huh.NewInput().Title("Totp Code (if setup)").Value(&iTotp), - ), - ) - var baseTheme *huh.Theme = huh.ThemeBase() - err := form.WithTheme(baseTheme).Run() - if err != nil { - log.Fatal().Err(err).Msg("Form failed") - } - } - - user, err := utils.ParseUser(iUser) - if err != nil { - log.Fatal().Err(err).Msg("Failed to parse user") - } - - if user.Username != iUsername { - log.Fatal().Msg("Username is incorrect") - } - - err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(iPassword)) - if err != nil { - log.Fatal().Msg("Password is incorrect") - } - - if user.TotpSecret == "" { - if iTotp != "" { - log.Warn().Msg("User does not have 2fa secret") - } - log.Info().Msg("User verified") - return - } - - ok := totp.Validate(iTotp, user.TotpSecret) - if !ok { - log.Fatal().Msg("Totp code incorrect") - - } - - log.Info().Msg("User verified") - }, -} - -func init() { - VerifyCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Create a user interactively") - VerifyCmd.Flags().BoolVar(&docker, "docker", false, "Is the user formatted for docker?") - VerifyCmd.Flags().StringVar(&iUsername, "username", "", "Username") - VerifyCmd.Flags().StringVar(&iPassword, "password", "", "Password") - VerifyCmd.Flags().StringVar(&iTotp, "totp", "", "Totp code") - VerifyCmd.Flags().StringVar(&iUser, "user", "", "Hash (username:hash:totp combination)") -} diff --git a/cmd/verify.go b/cmd/verify.go new file mode 100644 index 0000000..93b6a99 --- /dev/null +++ b/cmd/verify.go @@ -0,0 +1,118 @@ +package cmd + +import ( + "errors" + "tinyauth/internal/utils" + + "github.com/charmbracelet/huh" + "github.com/pquerna/otp/totp" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + "golang.org/x/crypto/bcrypt" +) + +type verifyUserCmd struct { + root *cobra.Command + cmd *cobra.Command + + interactive bool + username string + password string + totp string + user string +} + +func newVerifyUserCmd(root *cobra.Command) *verifyUserCmd { + return &verifyUserCmd{ + root: root, + } +} + +func (c *verifyUserCmd) Register() { + c.cmd = &cobra.Command{ + Use: "verify", + Short: "Verify a user is set up correctly", + Long: `Verify a user is set up correctly meaning that it has a correct username, password and TOTP code.`, + Run: c.run, + } + + c.cmd.Flags().BoolVarP(&c.interactive, "interactive", "i", false, "Validate a user interactively") + c.cmd.Flags().StringVar(&c.username, "username", "", "Username") + c.cmd.Flags().StringVar(&c.password, "password", "", "Password") + c.cmd.Flags().StringVar(&c.totp, "totp", "", "TOTP code") + c.cmd.Flags().StringVar(&c.user, "user", "", "Hash (username:hash:totp)") + + if c.root != nil { + c.root.AddCommand(c.cmd) + } +} + +func (c *verifyUserCmd) GetCmd() *cobra.Command { + return c.cmd +} + +func (c *verifyUserCmd) run(cmd *cobra.Command, args []string) { + log.Logger = log.Level(zerolog.InfoLevel) + + if c.interactive { + form := huh.NewForm( + huh.NewGroup( + huh.NewInput().Title("User (username:hash:totp)").Value(&c.user).Validate((func(s string) error { + if s == "" { + return errors.New("user cannot be empty") + } + return nil + })), + huh.NewInput().Title("Username").Value(&c.username).Validate((func(s string) error { + if s == "" { + return errors.New("username cannot be empty") + } + return nil + })), + huh.NewInput().Title("Password").Value(&c.password).Validate((func(s string) error { + if s == "" { + return errors.New("password cannot be empty") + } + return nil + })), + huh.NewInput().Title("TOTP Code (optional)").Value(&c.totp), + ), + ) + var baseTheme *huh.Theme = huh.ThemeBase() + err := form.WithTheme(baseTheme).Run() + if err != nil { + log.Fatal().Err(err).Msg("Form failed") + } + } + + user, err := utils.ParseUser(c.user) + if err != nil { + log.Fatal().Err(err).Msg("Failed to parse user") + } + + if user.Username != c.username { + log.Fatal().Msg("Username is incorrect") + } + + err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(c.password)) + if err != nil { + log.Fatal().Msg("Password is incorrect") + } + + if user.TotpSecret == "" { + if c.totp != "" { + log.Warn().Msg("User does not have TOTP secret") + } + log.Info().Msg("User verified") + return + } + + ok := totp.Validate(c.totp, user.TotpSecret) + if !ok { + log.Fatal().Msg("TOTP code incorrect") + + } + + log.Info().Msg("User verified") +} diff --git a/cmd/version.go b/cmd/version.go index 2a1827b..37eb14a 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -7,17 +7,36 @@ import ( "github.com/spf13/cobra" ) -var versionCmd = &cobra.Command{ - Use: "version", - Short: "Print the version number of Tinyauth", - Long: `All software has versions. This is Tinyauth's`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("Version: %s\n", config.Version) - fmt.Printf("Commit Hash: %s\n", config.CommitHash) - fmt.Printf("Build Timestamp: %s\n", config.BuildTimestamp) - }, +type versionCmd struct { + root *cobra.Command + cmd *cobra.Command } -func init() { - rootCmd.AddCommand(versionCmd) +func newVersionCmd(root *cobra.Command) *versionCmd { + return &versionCmd{ + root: root, + } +} + +func (c *versionCmd) Register() { + c.cmd = &cobra.Command{ + Use: "version", + Short: "Print the version number of Tinyauth", + Long: `All software has versions. This is Tinyauth's.`, + Run: c.run, + } + + if c.root != nil { + c.root.AddCommand(c.cmd) + } +} + +func (c *versionCmd) GetCmd() *cobra.Command { + return c.cmd +} + +func (c *versionCmd) run(cmd *cobra.Command, args []string) { + fmt.Printf("Version: %s\n", config.Version) + fmt.Printf("Commit Hash: %s\n", config.CommitHash) + fmt.Printf("Build Timestamp: %s\n", config.BuildTimestamp) } diff --git a/main.go b/main.go index 8126e9e..3632749 100644 --- a/main.go +++ b/main.go @@ -11,5 +11,5 @@ import ( func main() { log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Caller().Logger() - cmd.Execute() + cmd.Run() }