mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-10-31 06:05:43 +00:00 
			
		
		
		
	Compare commits
	
		
			4 Commits
		
	
	
		
			d38e7b9cea
			...
			402e7e565d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 402e7e565d | ||
|   | a629430a88 | ||
|   | f0a48cc91c | ||
|   | 2f8fa39a9b | 
| @@ -45,8 +45,6 @@ FROM alpine:3.22 AS runner | |||||||
|  |  | ||||||
| WORKDIR /tinyauth | WORKDIR /tinyauth | ||||||
|  |  | ||||||
| RUN apk add --no-cache curl |  | ||||||
|  |  | ||||||
| COPY --from=builder /tinyauth/tinyauth ./ | COPY --from=builder /tinyauth/tinyauth ./ | ||||||
|  |  | ||||||
| EXPOSE 3000 | EXPOSE 3000 | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| <div align="center"> | <div align="center"> | ||||||
|     <img alt="Tinyauth" title="Tinyauth" width="96" src="assets/logo-rounded.png"> |     <img alt="Tinyauth" title="Tinyauth" width="96" src="assets/logo-rounded.png"> | ||||||
|     <h1>Tinyauth</h1> |     <h1>Tinyauth</h1> | ||||||
|     <p>The easiest way to secure your apps with a login screen.</p> |     <p>The simplest way to protect your apps with a login screen.</p> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
| <div align="center"> | <div align="center"> | ||||||
| @@ -14,7 +14,7 @@ | |||||||
|  |  | ||||||
| <br /> | <br /> | ||||||
|  |  | ||||||
| Tinyauth is a simple authentication middleware that adds a simple login screen or OAuth with Google, Github and any provider to all of your docker apps. It supports all the popular proxies like Traefik, Nginx and Caddy. | 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 apps. It supports all the popular proxies like Traefik, Nginx and Caddy. | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										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.") | ||||||
|  | } | ||||||
							
								
								
									
										109
									
								
								cmd/health.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								cmd/health.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,109 @@ | |||||||
|  | package cmd | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  | 	"tinyauth/internal/config" | ||||||
|  |  | ||||||
|  | 	"github.com/rs/zerolog" | ||||||
|  | 	"github.com/rs/zerolog/log" | ||||||
|  | 	"github.com/spf13/cobra" | ||||||
|  | 	"github.com/spf13/viper" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type healthzResponse struct { | ||||||
|  | 	Status  string `json:"status"` | ||||||
|  | 	Message string `json:"message"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type healthCmd struct { | ||||||
|  | 	root *cobra.Command | ||||||
|  | 	cmd  *cobra.Command | ||||||
|  |  | ||||||
|  | 	viper  *viper.Viper | ||||||
|  | 	appUrl string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func newHealthCmd(root *cobra.Command) *healthCmd { | ||||||
|  | 	return &healthCmd{ | ||||||
|  | 		root:  root, | ||||||
|  | 		viper: viper.New(), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *healthCmd) Register() { | ||||||
|  | 	c.cmd = &cobra.Command{ | ||||||
|  | 		Use:   "health", | ||||||
|  | 		Short: "Health check", | ||||||
|  | 		Long:  `Use the health check endpoint to verify that Tinyauth is running and it's healthy.`, | ||||||
|  | 		Run:   c.run, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	c.viper.AutomaticEnv() | ||||||
|  | 	c.cmd.Flags().StringVar(&c.appUrl, "app-url", "http://localhost:3000", "The URL where the Tinyauth server is running on.") | ||||||
|  | 	c.viper.BindEnv("app-url", "APP_URL") | ||||||
|  | 	c.viper.BindPFlags(c.cmd.Flags()) | ||||||
|  |  | ||||||
|  | 	if c.root != nil { | ||||||
|  | 		c.root.AddCommand(c.cmd) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *healthCmd) GetCmd() *cobra.Command { | ||||||
|  | 	return c.cmd | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *healthCmd) run(cmd *cobra.Command, args []string) { | ||||||
|  | 	log.Logger = log.Level(zerolog.InfoLevel) | ||||||
|  |  | ||||||
|  | 	appUrl := c.viper.GetString("app-url") | ||||||
|  |  | ||||||
|  | 	if appUrl == "" { | ||||||
|  | 		log.Fatal().Err(errors.New("app-url is required")).Msg("App URL is required") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if config.Version == "development" { | ||||||
|  | 		log.Warn().Msg("Running in development mode. Overriding the app-url to http://localhost:3000") | ||||||
|  | 		appUrl = "http://localhost:3000" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log.Info().Msgf("Health check endpoint is available at %s/api/healthz", appUrl) | ||||||
|  |  | ||||||
|  | 	client := http.Client{} | ||||||
|  |  | ||||||
|  | 	req, err := http.NewRequest("GET", appUrl+"/api/healthz", nil) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Fatal().Err(err).Msg("Failed to create request") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	resp, err := client.Do(req) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Fatal().Err(err).Msg("Failed to perform request") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if resp.StatusCode != http.StatusOK { | ||||||
|  | 		log.Fatal().Err(errors.New("service is not healthy")).Msgf("Service is not healthy. Status code: %d", resp.StatusCode) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	defer resp.Body.Close() | ||||||
|  |  | ||||||
|  | 	var healthResp healthzResponse | ||||||
|  |  | ||||||
|  | 	body, err := io.ReadAll(resp.Body) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Fatal().Err(err).Msg("Failed to read response") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err = json.Unmarshal(body, &healthResp) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Fatal().Err(err).Msg("Failed to decode response") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log.Info().Interface("response", healthResp).Msg("Service is healthy") | ||||||
|  | } | ||||||
							
								
								
									
										142
									
								
								cmd/root.go
									
									
									
									
									
								
							
							
						
						
									
										142
									
								
								cmd/root.go
									
									
									
									
									
								
							| @@ -2,8 +2,6 @@ package cmd | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"strings" | 	"strings" | ||||||
| 	totpCmd "tinyauth/cmd/totp" |  | ||||||
| 	userCmd "tinyauth/cmd/user" |  | ||||||
| 	"tinyauth/internal/bootstrap" | 	"tinyauth/internal/bootstrap" | ||||||
| 	"tinyauth/internal/config" | 	"tinyauth/internal/config" | ||||||
| 	"tinyauth/internal/utils" | 	"tinyauth/internal/utils" | ||||||
| @@ -15,55 +13,28 @@ import ( | |||||||
| 	"github.com/spf13/viper" | 	"github.com/spf13/viper" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var rootCmd = &cobra.Command{ | type rootCmd struct { | ||||||
|  | 	root *cobra.Command | ||||||
|  | 	cmd  *cobra.Command | ||||||
|  |  | ||||||
|  | 	viper *viper.Viper | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func newRootCmd() *rootCmd { | ||||||
|  | 	return &rootCmd{ | ||||||
|  | 		viper: viper.New(), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *rootCmd) Register() { | ||||||
|  | 	c.cmd = &cobra.Command{ | ||||||
| 		Use:   "tinyauth", | 		Use:   "tinyauth", | ||||||
| 	Short: "The simplest way to protect your apps with a login screen.", | 		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.`, | 		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: func(cmd *cobra.Command, args []string) { | 		Run:   c.run, | ||||||
| 		var conf config.Config |  | ||||||
|  |  | ||||||
| 		err := viper.Unmarshal(&conf) |  | ||||||
| 		if err != nil { |  | ||||||
| 			log.Fatal().Err(err).Msg("Failed to parse config") |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 		// Validate config | 	c.viper.AutomaticEnv() | ||||||
| 		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") |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 	}, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func Execute() { |  | ||||||
| 	rootCmd.FParseErrWhitelist.UnknownFlags = true |  | ||||||
| 	err := rootCmd.Execute() |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Fatal().Err(err).Msg("Failed to execute command") |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	rootCmd.AddCommand(userCmd.UserCmd()) |  | ||||||
| 	rootCmd.AddCommand(totpCmd.TotpCmd()) |  | ||||||
|  |  | ||||||
| 	viper.AutomaticEnv() |  | ||||||
|  |  | ||||||
| 	configOptions := []struct { | 	configOptions := []struct { | ||||||
| 		name        string | 		name        string | ||||||
| @@ -101,17 +72,82 @@ func init() { | |||||||
| 	for _, opt := range configOptions { | 	for _, opt := range configOptions { | ||||||
| 		switch v := opt.defaultVal.(type) { | 		switch v := opt.defaultVal.(type) { | ||||||
| 		case bool: | 		case bool: | ||||||
| 			rootCmd.Flags().Bool(opt.name, v, opt.description) | 			c.cmd.Flags().Bool(opt.name, v, opt.description) | ||||||
| 		case int: | 		case int: | ||||||
| 			rootCmd.Flags().Int(opt.name, v, opt.description) | 			c.cmd.Flags().Int(opt.name, v, opt.description) | ||||||
| 		case string: | 		case string: | ||||||
| 			rootCmd.Flags().String(opt.name, v, opt.description) | 			c.cmd.Flags().String(opt.name, v, opt.description) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Create uppercase env var name | 		// Create uppercase env var name | ||||||
| 		envVar := strings.ReplaceAll(strings.ToUpper(opt.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() | ||||||
|  | 	newHealthCmd(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" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var versionCmd = &cobra.Command{ | type versionCmd struct { | ||||||
|  | 	root *cobra.Command | ||||||
|  | 	cmd  *cobra.Command | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func newVersionCmd(root *cobra.Command) *versionCmd { | ||||||
|  | 	return &versionCmd{ | ||||||
|  | 		root: root, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *versionCmd) Register() { | ||||||
|  | 	c.cmd = &cobra.Command{ | ||||||
| 		Use:   "version", | 		Use:   "version", | ||||||
| 		Short: "Print the version number of Tinyauth", | 		Short: "Print the version number of Tinyauth", | ||||||
| 	Long:  `All software has versions. This is Tinyauth's`, | 		Long:  `All software has versions. This is Tinyauth's.`, | ||||||
| 	Run: func(cmd *cobra.Command, args []string) { | 		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("Version: %s\n", config.Version) | ||||||
| 	fmt.Printf("Commit Hash: %s\n", config.CommitHash) | 	fmt.Printf("Commit Hash: %s\n", config.CommitHash) | ||||||
| 	fmt.Printf("Build Timestamp: %s\n", config.BuildTimestamp) | 	fmt.Printf("Build Timestamp: %s\n", config.BuildTimestamp) | ||||||
| 	}, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	rootCmd.AddCommand(versionCmd) |  | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.mod
									
									
									
									
									
								
							| @@ -8,7 +8,7 @@ require ( | |||||||
| 	github.com/cenkalti/backoff/v5 v5.0.3 | 	github.com/cenkalti/backoff/v5 v5.0.3 | ||||||
| 	github.com/gin-gonic/gin v1.11.0 | 	github.com/gin-gonic/gin v1.11.0 | ||||||
| 	github.com/glebarez/sqlite v1.11.0 | 	github.com/glebarez/sqlite v1.11.0 | ||||||
| 	github.com/go-playground/validator/v10 v10.27.0 | 	github.com/go-playground/validator/v10 v10.28.0 | ||||||
| 	github.com/golang-migrate/migrate/v4 v4.19.0 | 	github.com/golang-migrate/migrate/v4 v4.19.0 | ||||||
| 	github.com/google/go-querystring v1.1.0 | 	github.com/google/go-querystring v1.1.0 | ||||||
| 	github.com/google/uuid v1.6.0 | 	github.com/google/uuid v1.6.0 | ||||||
| @@ -87,7 +87,7 @@ require ( | |||||||
| 	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect | 	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect | ||||||
| 	github.com/felixge/httpsnoop v1.0.4 // indirect | 	github.com/felixge/httpsnoop v1.0.4 // indirect | ||||||
| 	github.com/fsnotify/fsnotify v1.9.0 // indirect | 	github.com/fsnotify/fsnotify v1.9.0 // indirect | ||||||
| 	github.com/gabriel-vasile/mimetype v1.4.8 // indirect | 	github.com/gabriel-vasile/mimetype v1.4.10 // indirect | ||||||
| 	github.com/gin-contrib/sse v1.1.0 // indirect | 	github.com/gin-contrib/sse v1.1.0 // indirect | ||||||
| 	github.com/go-ldap/ldap/v3 v3.4.12 | 	github.com/go-ldap/ldap/v3 v3.4.12 | ||||||
| 	github.com/go-logr/logr v1.4.3 // indirect | 	github.com/go-logr/logr v1.4.3 // indirect | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								go.sum
									
									
									
									
									
								
							| @@ -88,8 +88,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk | |||||||
| github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= | ||||||
| github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= | ||||||
| github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= | github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= | ||||||
| github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= | github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= | ||||||
| github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= | github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= | ||||||
| github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= | github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= | ||||||
| github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= | github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= | ||||||
| github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= | github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= | ||||||
| @@ -113,8 +113,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o | |||||||
| github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= | ||||||
| github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= | ||||||
| github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= | ||||||
| github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= | github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= | ||||||
| github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= | github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= | ||||||
| github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= | github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= | ||||||
| github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= | github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= | ||||||
| github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= | github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= | ||||||
|   | |||||||
| @@ -13,8 +13,8 @@ func NewHealthController(router *gin.RouterGroup) *HealthController { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (controller *HealthController) SetupRoutes() { | func (controller *HealthController) SetupRoutes() { | ||||||
| 	controller.router.GET("/health", controller.healthHandler) | 	controller.router.GET("/healthz", controller.healthHandler) | ||||||
| 	controller.router.HEAD("/health", controller.healthHandler) | 	controller.router.HEAD("/healthz", controller.healthHandler) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (controller *HealthController) healthHandler(c *gin.Context) { | func (controller *HealthController) healthHandler(c *gin.Context) { | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								main.go
									
									
									
									
									
								
							| @@ -11,5 +11,5 @@ import ( | |||||||
|  |  | ||||||
| func main() { | func main() { | ||||||
| 	log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Caller().Logger() | 	log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Caller().Logger() | ||||||
| 	cmd.Execute() | 	cmd.Run() | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user