mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-10-31 14:15:50 +00:00 
			
		
		
		
	Compare commits
	
		
			4 Commits
		
	
	
		
			3960d89788
			...
			refactor/c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | e1dcbda4ec | ||
|   | fa62ba2e33 | ||
|   | 04b075c748 | ||
|   | 74a360369d | 
							
								
								
									
										99
									
								
								cmd/create.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								cmd/create.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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") | ||||
| } | ||||
							
								
								
									
										120
									
								
								cmd/generate.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								cmd/generate.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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.") | ||||
| } | ||||
							
								
								
									
										133
									
								
								cmd/root.go
									
									
									
									
									
								
							
							
						
						
									
										133
									
								
								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") | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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") | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
| @@ -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") | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
| @@ -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)") | ||||
| } | ||||
							
								
								
									
										118
									
								
								cmd/verify.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								cmd/verify.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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") | ||||
| } | ||||
| @@ -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) | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user