mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-11-03 23:55:44 +00:00 
			
		
		
		
	Compare commits
	
		
			4 Commits
		
	
	
		
			v4.0.0-bet
			...
			402e7e565d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					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