mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-10-31 06:05:43 +00:00 
			
		
		
		
	Compare commits
	
		
			24 Commits
		
	
	
		
			v3.0.0
			...
			v3.1.0-exp
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 38105d0b4e | ||
|   | e13bd14eb6 | ||
|   | 43dc3f9aa6 | ||
|   | 00bfaa1cbe | ||
|   | 8cc0f8b31b | ||
|   | 631059be69 | ||
|   | 5188089673 | ||
|   | 47fff12bac | ||
|   | a8c51b649f | ||
|   | c2e8f1b473 | ||
|   | bdf327cc9a | ||
|   | 46ec623d74 | ||
|   | f97c4d7e78 | ||
|   | d45b148725 | ||
|   | 33904f7f86 | ||
|   | 7e0bc84b0f | ||
|   | fc3f8b5036 | ||
|   | 3030fc5fcf | ||
|   | e4379cf3ed | ||
|   | 30aab17f06 | ||
|   | 7ee0b645e6 | ||
|   | 5c34ab96a9 | ||
|   | cb6f93d879 | ||
|   | df0c356511 | 
							
								
								
									
										137
									
								
								.github/workflows/experimental-build.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								.github/workflows/experimental-build.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,137 @@ | |||||||
|  | name: Experimental Build | ||||||
|  | on: | ||||||
|  |   workflow_dispatch: | ||||||
|  |   push: | ||||||
|  |     tags: | ||||||
|  |       - "v*" | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   build: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout | ||||||
|  |         uses: actions/checkout@v4 | ||||||
|  |  | ||||||
|  |       - name: Docker meta | ||||||
|  |         id: meta | ||||||
|  |         uses: docker/metadata-action@v5 | ||||||
|  |         with: | ||||||
|  |           images: ghcr.io/${{ github.repository_owner }}/tinyauth | ||||||
|  |  | ||||||
|  |       - name: Login to GitHub Container Registry | ||||||
|  |         uses: docker/login-action@v3 | ||||||
|  |         with: | ||||||
|  |           registry: ghcr.io | ||||||
|  |           username: ${{ github.repository_owner }} | ||||||
|  |           password: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  |  | ||||||
|  |       - name: Set up Docker Buildx | ||||||
|  |         uses: docker/setup-buildx-action@v3 | ||||||
|  |  | ||||||
|  |       - name: Build and push | ||||||
|  |         uses: docker/build-push-action@v6 | ||||||
|  |         id: build | ||||||
|  |         with: | ||||||
|  |           platforms: linux/amd64 | ||||||
|  |           labels: ${{ steps.meta.outputs.labels }} | ||||||
|  |           tags: ghcr.io/${{ github.repository_owner }}/tinyauth | ||||||
|  |           outputs: type=image,push-by-digest=true,name-canonical=true,push=true | ||||||
|  |  | ||||||
|  |       - name: Export digest | ||||||
|  |         run: | | ||||||
|  |           mkdir -p ${{ runner.temp }}/digests | ||||||
|  |           digest="${{ steps.build.outputs.digest }}" | ||||||
|  |           touch "${{ runner.temp }}/digests/${digest#sha256:}" | ||||||
|  |  | ||||||
|  |       - name: Upload digest | ||||||
|  |         uses: actions/upload-artifact@v4 | ||||||
|  |         with: | ||||||
|  |           name: digests-linux-amd64 | ||||||
|  |           path: ${{ runner.temp }}/digests/* | ||||||
|  |           if-no-files-found: error | ||||||
|  |           retention-days: 1 | ||||||
|  |  | ||||||
|  |   build-arm: | ||||||
|  |     runs-on: ubuntu-24.04-arm | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout | ||||||
|  |         uses: actions/checkout@v4 | ||||||
|  |  | ||||||
|  |       - name: Docker meta | ||||||
|  |         id: meta | ||||||
|  |         uses: docker/metadata-action@v5 | ||||||
|  |         with: | ||||||
|  |           images: ghcr.io/${{ github.repository_owner }}/tinyauth | ||||||
|  |  | ||||||
|  |       - name: Login to GitHub Container Registry | ||||||
|  |         uses: docker/login-action@v3 | ||||||
|  |         with: | ||||||
|  |           registry: ghcr.io | ||||||
|  |           username: ${{ github.repository_owner }} | ||||||
|  |           password: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  |  | ||||||
|  |       - name: Set up Docker Buildx | ||||||
|  |         uses: docker/setup-buildx-action@v3 | ||||||
|  |  | ||||||
|  |       - name: Build and push | ||||||
|  |         uses: docker/build-push-action@v6 | ||||||
|  |         id: build | ||||||
|  |         with: | ||||||
|  |           platforms: linux/arm64 | ||||||
|  |           labels: ${{ steps.meta.outputs.labels }} | ||||||
|  |           tags: ghcr.io/${{ github.repository_owner }}/tinyauth | ||||||
|  |           outputs: type=image,push-by-digest=true,name-canonical=true,push=true | ||||||
|  |  | ||||||
|  |       - name: Export digest | ||||||
|  |         run: | | ||||||
|  |           mkdir -p ${{ runner.temp }}/digests | ||||||
|  |           digest="${{ steps.build.outputs.digest }}" | ||||||
|  |           touch "${{ runner.temp }}/digests/${digest#sha256:}" | ||||||
|  |  | ||||||
|  |       - name: Upload digest | ||||||
|  |         uses: actions/upload-artifact@v4 | ||||||
|  |         with: | ||||||
|  |           name: digests-linux-arm64 | ||||||
|  |           path: ${{ runner.temp }}/digests/* | ||||||
|  |           if-no-files-found: error | ||||||
|  |           retention-days: 1 | ||||||
|  |  | ||||||
|  |   merge: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     needs: | ||||||
|  |       - build | ||||||
|  |       - build-arm | ||||||
|  |     steps: | ||||||
|  |       - name: Download digests | ||||||
|  |         uses: actions/download-artifact@v4 | ||||||
|  |         with: | ||||||
|  |           path: ${{ runner.temp }}/digests | ||||||
|  |           pattern: digests-* | ||||||
|  |           merge-multiple: true | ||||||
|  |  | ||||||
|  |       - name: Login to GitHub Container Registry | ||||||
|  |         uses: docker/login-action@v3 | ||||||
|  |         with: | ||||||
|  |           registry: ghcr.io | ||||||
|  |           username: ${{ github.repository_owner }} | ||||||
|  |           password: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  |  | ||||||
|  |       - name: Set up Docker Buildx | ||||||
|  |         uses: docker/setup-buildx-action@v3 | ||||||
|  |  | ||||||
|  |       - name: Docker meta | ||||||
|  |         id: meta | ||||||
|  |         uses: docker/metadata-action@v5 | ||||||
|  |         with: | ||||||
|  |           images: ghcr.io/${{ github.repository_owner }}/tinyauth | ||||||
|  |           tags: | | ||||||
|  |             type=ref,event=branch | ||||||
|  |             type=ref,event=pr | ||||||
|  |             type=semver,pattern={{version}} | ||||||
|  |             type=semver,pattern={{major}}.{{minor}} | ||||||
|  |  | ||||||
|  |       - name: Create manifest list and push | ||||||
|  |         working-directory: ${{ runner.temp }}/digests | ||||||
|  |         run: | | ||||||
|  |           docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ | ||||||
|  |             $(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *) | ||||||
							
								
								
									
										2
									
								
								FUNDING.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								FUNDING.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | github: steveiliop56 | ||||||
|  | buy_me_a_coffee: steveiliop56 | ||||||
							
								
								
									
										25
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | # Build website | ||||||
|  | web: | ||||||
|  | 	cd site; bun run build | ||||||
|  |  | ||||||
|  | # Copy site assets | ||||||
|  | assets: web | ||||||
|  | 	rm -rf internal/assets/dist | ||||||
|  | 	mkdir -p internal/assets/dist | ||||||
|  | 	cp -r site/dist/* internal/assets/dist | ||||||
|  |  | ||||||
|  | # Run development binary | ||||||
|  | run: assets | ||||||
|  | 	go run main.go | ||||||
|  |  | ||||||
|  | # Test | ||||||
|  | test: | ||||||
|  | 	go test ./... | ||||||
|  |  | ||||||
|  | # Build | ||||||
|  | build: assets | ||||||
|  | 	go build -o tinyauth | ||||||
|  |  | ||||||
|  | # Build no site | ||||||
|  | build-skip-web: | ||||||
|  | 	go build -o tinyauth | ||||||
							
								
								
									
										10
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								README.md
									
									
									
									
									
								
							| @@ -42,9 +42,17 @@ All contributions to the codebase are welcome! If you have any recommendations o | |||||||
|  |  | ||||||
| Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build & install instructions. For more information about the license check the [license](./LICENSE) file. | Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build & install instructions. For more information about the license check the [license](./LICENSE) file. | ||||||
|  |  | ||||||
|  | ## Sponsors | ||||||
|  |  | ||||||
|  | Thanks a lot to the following people for providing me with more coffee: | ||||||
|  |  | ||||||
|  | | <img height="64" src="https://avatars.githubusercontent.com/u/47644445?v=4" alt="Nicolas"> | <img height="64" src="https://avatars.githubusercontent.com/u/4255748?v=4" alt="Erwin"> | | ||||||
|  | | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | | ||||||
|  | | <div align="center"><a href="https://github.com/nicotsx">Nicolas</a></div>                 | <div align="center"><a href="https://github.com/erwinkramer">Erwin</a></div>            | | ||||||
|  |  | ||||||
| ## Acknowledgements | ## Acknowledgements | ||||||
|  |  | ||||||
| Credits for the logo of this app go to: | Credits for the logo of this app go to: | ||||||
|  |  | ||||||
| - **Freepik** for providing the police hat and logo. | - **Freepik** for providing the police hat and badge. | ||||||
| - **Renee French** for the original gopher logo. | - **Renee French** for the original gopher logo. | ||||||
|   | |||||||
							
								
								
									
										16
									
								
								cmd/root.go
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								cmd/root.go
									
									
									
									
									
								
							| @@ -5,7 +5,8 @@ import ( | |||||||
| 	"os" | 	"os" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| 	cmd "tinyauth/cmd/user" | 	totpCmd "tinyauth/cmd/totp" | ||||||
|  | 	userCmd "tinyauth/cmd/user" | ||||||
| 	"tinyauth/internal/api" | 	"tinyauth/internal/api" | ||||||
| 	"tinyauth/internal/assets" | 	"tinyauth/internal/assets" | ||||||
| 	"tinyauth/internal/auth" | 	"tinyauth/internal/auth" | ||||||
| @@ -113,7 +114,9 @@ var rootCmd = &cobra.Command{ | |||||||
| 			AppURL:          config.AppURL, | 			AppURL:          config.AppURL, | ||||||
| 			CookieSecure:    config.CookieSecure, | 			CookieSecure:    config.CookieSecure, | ||||||
| 			DisableContinue: config.DisableContinue, | 			DisableContinue: config.DisableContinue, | ||||||
| 			CookieExpiry:    config.SessionExpiry, | 			SessionExpiry:   config.SessionExpiry, | ||||||
|  | 			Title:           config.Title, | ||||||
|  | 			GenericName:     config.GenericName, | ||||||
| 		}, hooks, auth, providers) | 		}, hooks, auth, providers) | ||||||
|  |  | ||||||
| 		// Setup routes | 		// Setup routes | ||||||
| @@ -139,7 +142,10 @@ func HandleError(err error, msg string) { | |||||||
|  |  | ||||||
| func init() { | func init() { | ||||||
| 	// Add user command | 	// Add user command | ||||||
| 	rootCmd.AddCommand(cmd.UserCmd()) | 	rootCmd.AddCommand(userCmd.UserCmd()) | ||||||
|  |  | ||||||
|  | 	// Add totp command | ||||||
|  | 	rootCmd.AddCommand(totpCmd.TotpCmd()) | ||||||
|  |  | ||||||
| 	// Read environment variables | 	// Read environment variables | ||||||
| 	viper.AutomaticEnv() | 	viper.AutomaticEnv() | ||||||
| @@ -169,10 +175,12 @@ func init() { | |||||||
| 	rootCmd.Flags().String("generic-auth-url", "", "Generic OAuth auth URL.") | 	rootCmd.Flags().String("generic-auth-url", "", "Generic OAuth auth URL.") | ||||||
| 	rootCmd.Flags().String("generic-token-url", "", "Generic OAuth token URL.") | 	rootCmd.Flags().String("generic-token-url", "", "Generic OAuth token URL.") | ||||||
| 	rootCmd.Flags().String("generic-user-url", "", "Generic OAuth user info URL.") | 	rootCmd.Flags().String("generic-user-url", "", "Generic OAuth user info URL.") | ||||||
|  | 	rootCmd.Flags().String("generic-name", "Generic", "Generic OAuth provider name.") | ||||||
| 	rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.") | 	rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.") | ||||||
| 	rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.") | 	rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.") | ||||||
| 	rootCmd.Flags().Int("session-expiry", 86400, "Session (cookie) expiration time in seconds.") | 	rootCmd.Flags().Int("session-expiry", 86400, "Session (cookie) expiration time in seconds.") | ||||||
| 	rootCmd.Flags().Int("log-level", 1, "Log level.") | 	rootCmd.Flags().Int("log-level", 1, "Log level.") | ||||||
|  | 	rootCmd.Flags().String("app-title", "Tinyauth", "Title of the app.") | ||||||
|  |  | ||||||
| 	// Bind flags to environment | 	// Bind flags to environment | ||||||
| 	viper.BindEnv("port", "PORT") | 	viper.BindEnv("port", "PORT") | ||||||
| @@ -199,10 +207,12 @@ func init() { | |||||||
| 	viper.BindEnv("generic-auth-url", "GENERIC_AUTH_URL") | 	viper.BindEnv("generic-auth-url", "GENERIC_AUTH_URL") | ||||||
| 	viper.BindEnv("generic-token-url", "GENERIC_TOKEN_URL") | 	viper.BindEnv("generic-token-url", "GENERIC_TOKEN_URL") | ||||||
| 	viper.BindEnv("generic-user-url", "GENERIC_USER_URL") | 	viper.BindEnv("generic-user-url", "GENERIC_USER_URL") | ||||||
|  | 	viper.BindEnv("generic-name", "GENERIC_NAME") | ||||||
| 	viper.BindEnv("disable-continue", "DISABLE_CONTINUE") | 	viper.BindEnv("disable-continue", "DISABLE_CONTINUE") | ||||||
| 	viper.BindEnv("oauth-whitelist", "OAUTH_WHITELIST") | 	viper.BindEnv("oauth-whitelist", "OAUTH_WHITELIST") | ||||||
| 	viper.BindEnv("session-expiry", "SESSION_EXPIRY") | 	viper.BindEnv("session-expiry", "SESSION_EXPIRY") | ||||||
| 	viper.BindEnv("log-level", "LOG_LEVEL") | 	viper.BindEnv("log-level", "LOG_LEVEL") | ||||||
|  | 	viper.BindEnv("app-title", "APP_TITLE") | ||||||
|  |  | ||||||
| 	// Bind flags to viper | 	// Bind flags to viper | ||||||
| 	viper.BindPFlags(rootCmd.Flags()) | 	viper.BindPFlags(rootCmd.Flags()) | ||||||
|   | |||||||
							
								
								
									
										121
									
								
								cmd/totp/generate/generate.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								cmd/totp/generate/generate.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | |||||||
|  | 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" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Interactive flag | ||||||
|  | var interactive bool | ||||||
|  |  | ||||||
|  | // i stands for input | ||||||
|  | var iUser string | ||||||
|  |  | ||||||
|  | var GenerateCmd = &cobra.Command{ | ||||||
|  | 	Use:   "generate", | ||||||
|  | 	Short: "Generate a totp secret", | ||||||
|  | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
|  | 		// Setup logger | ||||||
|  | 		log.Logger = log.Level(zerolog.InfoLevel) | ||||||
|  |  | ||||||
|  | 		// Use simple theme | ||||||
|  | 		var baseTheme *huh.Theme = huh.ThemeBase() | ||||||
|  |  | ||||||
|  | 		// Interactive | ||||||
|  | 		if interactive { | ||||||
|  | 			// Create huh form | ||||||
|  | 			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 | ||||||
|  | 					})), | ||||||
|  | 				), | ||||||
|  | 			) | ||||||
|  |  | ||||||
|  | 			// Run form | ||||||
|  | 			formErr := form.WithTheme(baseTheme).Run() | ||||||
|  |  | ||||||
|  | 			if formErr != nil { | ||||||
|  | 				log.Fatal().Err(formErr).Msg("Form failed") | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Parse user | ||||||
|  | 		user, parseErr := utils.ParseUser(iUser) | ||||||
|  |  | ||||||
|  | 		if parseErr != nil { | ||||||
|  | 			log.Fatal().Err(parseErr).Msg("Failed to parse user") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Check if user was using docker escape | ||||||
|  | 		dockerEscape := false | ||||||
|  |  | ||||||
|  | 		if strings.Contains(iUser, "$$") { | ||||||
|  | 			dockerEscape = true | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Check it has totp | ||||||
|  | 		if user.TotpSecret != "" { | ||||||
|  | 			log.Fatal().Msg("User already has a totp secret") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Generate totp secret | ||||||
|  | 		key, keyErr := totp.Generate(totp.GenerateOpts{ | ||||||
|  | 			Issuer:      "Tinyauth", | ||||||
|  | 			AccountName: user.Username, | ||||||
|  | 		}) | ||||||
|  |  | ||||||
|  | 		if keyErr != nil { | ||||||
|  | 			log.Fatal().Err(keyErr).Msg("Failed to generate totp secret") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Create secret | ||||||
|  | 		secret := key.Secret() | ||||||
|  |  | ||||||
|  | 		// Print secret and image | ||||||
|  | 		log.Info().Str("secret", secret).Msg("Generated totp secret") | ||||||
|  |  | ||||||
|  | 		// Print QR code | ||||||
|  | 		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) | ||||||
|  |  | ||||||
|  | 		// Add the secret to the user | ||||||
|  | 		user.TotpSecret = secret | ||||||
|  |  | ||||||
|  | 		// If using docker escape re-escape it | ||||||
|  | 		if dockerEscape { | ||||||
|  | 			user.Password = strings.ReplaceAll(user.Password, "$", "$$") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Print success | ||||||
|  | 		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() { | ||||||
|  | 	// Add interactive flag | ||||||
|  | 	GenerateCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Run in interactive mode") | ||||||
|  | 	GenerateCmd.Flags().StringVar(&iUser, "user", "", "Your current username:hash") | ||||||
|  | } | ||||||
							
								
								
									
										22
									
								
								cmd/totp/totp.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								cmd/totp/totp.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | package cmd | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"tinyauth/cmd/totp/generate" | ||||||
|  |  | ||||||
|  | 	"github.com/spf13/cobra" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TotpCmd() *cobra.Command { | ||||||
|  | 	// Create the totp command | ||||||
|  | 	totpCmd := &cobra.Command{ | ||||||
|  | 		Use:   "totp", | ||||||
|  | 		Short: "Totp utilities", | ||||||
|  | 		Long:  `Utilities for creating and verifying totp codes.`, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Add the generate command | ||||||
|  | 	totpCmd.AddCommand(generate.GenerateCmd) | ||||||
|  |  | ||||||
|  | 	// Return the totp command | ||||||
|  | 	return totpCmd | ||||||
|  | } | ||||||
| @@ -60,7 +60,7 @@ var CreateCmd = &cobra.Command{ | |||||||
|  |  | ||||||
| 		// Do we have username and password? | 		// Do we have username and password? | ||||||
| 		if iUsername == "" || iPassword == "" { | 		if iUsername == "" || iPassword == "" { | ||||||
| 			log.Error().Msg("Username and password cannot be empty") | 			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") | 		log.Info().Str("username", iUsername).Str("password", iPassword).Bool("docker", docker).Msg("Creating user") | ||||||
|   | |||||||
| @@ -2,9 +2,10 @@ package verify | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"strings" | 	"tinyauth/internal/utils" | ||||||
|  |  | ||||||
| 	"github.com/charmbracelet/huh" | 	"github.com/charmbracelet/huh" | ||||||
|  | 	"github.com/pquerna/otp/totp" | ||||||
| 	"github.com/rs/zerolog" | 	"github.com/rs/zerolog" | ||||||
| 	"github.com/rs/zerolog/log" | 	"github.com/rs/zerolog/log" | ||||||
| 	"github.com/spf13/cobra" | 	"github.com/spf13/cobra" | ||||||
| @@ -17,22 +18,26 @@ var docker bool | |||||||
| // i stands for input | // i stands for input | ||||||
| var iUsername string | var iUsername string | ||||||
| var iPassword string | var iPassword string | ||||||
|  | var iTotp string | ||||||
| var iUser string | var iUser string | ||||||
|  |  | ||||||
| var VerifyCmd = &cobra.Command{ | var VerifyCmd = &cobra.Command{ | ||||||
| 	Use:   "verify", | 	Use:   "verify", | ||||||
| 	Short: "Verify a user is set up correctly", | 	Short: "Verify a user is set up correctly", | ||||||
| 	Long:  `Verify a user is set up correctly meaning that it has a correct username and password.`, | 	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) { | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 		// Setup logger | 		// Setup logger | ||||||
| 		log.Logger = log.Level(zerolog.InfoLevel) | 		log.Logger = log.Level(zerolog.InfoLevel) | ||||||
|  |  | ||||||
|  | 		// Use simple theme | ||||||
|  | 		var baseTheme *huh.Theme = huh.ThemeBase() | ||||||
|  |  | ||||||
| 		// Check if interactive | 		// Check if interactive | ||||||
| 		if interactive { | 		if interactive { | ||||||
| 			// Create huh form | 			// Create huh form | ||||||
| 			form := huh.NewForm( | 			form := huh.NewForm( | ||||||
| 				huh.NewGroup( | 				huh.NewGroup( | ||||||
| 					huh.NewInput().Title("User (username:hash)").Value(&iUser).Validate((func(s string) error { | 					huh.NewInput().Title("User (username:hash:totp)").Value(&iUser).Validate((func(s string) error { | ||||||
| 						if s == "" { | 						if s == "" { | ||||||
| 							return errors.New("user cannot be empty") | 							return errors.New("user cannot be empty") | ||||||
| 						} | 						} | ||||||
| @@ -50,13 +55,11 @@ var VerifyCmd = &cobra.Command{ | |||||||
| 						} | 						} | ||||||
| 						return nil | 						return nil | ||||||
| 					})), | 					})), | ||||||
| 					huh.NewSelect[bool]().Title("Is the user formatted for docker?").Options(huh.NewOption("Yes", true), huh.NewOption("No", false)).Value(&docker), | 					huh.NewInput().Title("Totp Code (if setup)").Value(&iTotp), | ||||||
| 				), | 				), | ||||||
| 			) | 			) | ||||||
|  |  | ||||||
| 			// Use simple theme | 			// Run form | ||||||
| 			var baseTheme *huh.Theme = huh.ThemeBase() |  | ||||||
|  |  | ||||||
| 			formErr := form.WithTheme(baseTheme).Run() | 			formErr := form.WithTheme(baseTheme).Run() | ||||||
|  |  | ||||||
| 			if formErr != nil { | 			if formErr != nil { | ||||||
| @@ -64,33 +67,44 @@ var VerifyCmd = &cobra.Command{ | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Do we have username, password and user? | 		// Parse user | ||||||
| 		if iUsername == "" || iPassword == "" || iUser == "" { | 		user, userErr := utils.ParseUser(iUser) | ||||||
| 			log.Fatal().Msg("Username, password and user cannot be empty") |  | ||||||
|  | 		if userErr != nil { | ||||||
|  | 			log.Fatal().Err(userErr).Msg("Failed to parse user") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		log.Info().Str("user", iUser).Str("username", iUsername).Str("password", iPassword).Bool("docker", docker).Msg("Verifying user") | 		// Compare username | ||||||
|  | 		if user.Username != iUsername { | ||||||
| 		// Split username and password hash | 			log.Fatal().Msg("Username is incorrect") | ||||||
| 		username, hash, ok := strings.Cut(iUser, ":") |  | ||||||
|  |  | ||||||
| 		if !ok { |  | ||||||
| 			log.Fatal().Msg("User is not formatted correctly") |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Replace $$ with $ if formatted for docker | 		// Compare password | ||||||
| 		if docker { | 		verifyErr := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(iPassword)) | ||||||
| 			hash = strings.ReplaceAll(hash, "$$", "$") |  | ||||||
|  | 		if verifyErr != nil { | ||||||
|  | 			log.Fatal().Msg("Ppassword is incorrect") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Compare username and password | 		// Check if user has 2fa code | ||||||
| 		verifyErr := bcrypt.CompareHashAndPassword([]byte(hash), []byte(iPassword)) | 		if user.TotpSecret == "" { | ||||||
|  | 			if iTotp != "" { | ||||||
| 		if verifyErr != nil || username != iUsername { | 				log.Warn().Msg("User does not have 2fa secret") | ||||||
| 			log.Fatal().Msg("Username or password incorrect") |  | ||||||
| 		} else { |  | ||||||
| 			log.Info().Msg("Verification successful") |  | ||||||
| 			} | 			} | ||||||
|  | 			log.Info().Msg("User verified") | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Check totp code | ||||||
|  | 		totpOk := totp.Validate(iTotp, user.TotpSecret) | ||||||
|  |  | ||||||
|  | 		if !totpOk { | ||||||
|  | 			log.Fatal().Msg("Totp code incorrect") | ||||||
|  |  | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Done | ||||||
|  | 		log.Info().Msg("User verified") | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -100,5 +114,6 @@ func init() { | |||||||
| 	VerifyCmd.Flags().BoolVar(&docker, "docker", false, "Is the user formatted for docker?") | 	VerifyCmd.Flags().BoolVar(&docker, "docker", false, "Is the user formatted for docker?") | ||||||
| 	VerifyCmd.Flags().StringVar(&iUsername, "username", "", "Username") | 	VerifyCmd.Flags().StringVar(&iUsername, "username", "", "Username") | ||||||
| 	VerifyCmd.Flags().StringVar(&iPassword, "password", "", "Password") | 	VerifyCmd.Flags().StringVar(&iPassword, "password", "", "Password") | ||||||
| 	VerifyCmd.Flags().StringVar(&iUser, "user", "", "Hash (username:hash combination)") | 	VerifyCmd.Flags().StringVar(&iTotp, "totp", "", "Totp code") | ||||||
|  | 	VerifyCmd.Flags().StringVar(&iUser, "user", "", "Hash (username:hash:totp combination)") | ||||||
| } | } | ||||||
|   | |||||||
| @@ -30,4 +30,5 @@ services: | |||||||
|       traefik.enable: true |       traefik.enable: true | ||||||
|       traefik.http.routers.tinyauth.rule: Host(`tinyauth.dev.local`) |       traefik.http.routers.tinyauth.rule: Host(`tinyauth.dev.local`) | ||||||
|       traefik.http.services.tinyauth.loadbalancer.server.port: 3000 |       traefik.http.services.tinyauth.loadbalancer.server.port: 3000 | ||||||
|       traefik.http.middlewares.tinyauth.forwardauth.address: http://tinyauth:3000/api/auth |       traefik.http.middlewares.tinyauth.forwardauth.address: http://tinyauth:3000/api/auth/traefik | ||||||
|  |       traefik.http.middlewares.tinyauth.forwardauth.authResponseHeaders: Remote-User | ||||||
|   | |||||||
| @@ -28,4 +28,5 @@ services: | |||||||
|       traefik.enable: true |       traefik.enable: true | ||||||
|       traefik.http.routers.tinyauth.rule: Host(`tinyauth.example.com`) |       traefik.http.routers.tinyauth.rule: Host(`tinyauth.example.com`) | ||||||
|       traefik.http.services.tinyauth.loadbalancer.server.port: 3000 |       traefik.http.services.tinyauth.loadbalancer.server.port: 3000 | ||||||
|       traefik.http.middlewares.tinyauth.forwardauth.address: http://tinyauth:3000/api/auth |       traefik.http.middlewares.tinyauth.forwardauth.address: http://tinyauth:3000/api/auth/traefik | ||||||
|  |       traefik.http.middlewares.tinyauth.forwardauth.authResponseHeaders: Remote-User | ||||||
|   | |||||||
							
								
								
									
										31
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								go.mod
									
									
									
									
									
								
							| @@ -13,23 +13,37 @@ require ( | |||||||
| 	golang.org/x/crypto v0.32.0 | 	golang.org/x/crypto v0.32.0 | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | require ( | ||||||
|  | 	github.com/containerd/log v0.1.0 // indirect | ||||||
|  | 	github.com/mdp/qrterminal/v3 v3.2.0 // indirect | ||||||
|  | 	github.com/moby/term v0.5.2 // indirect | ||||||
|  | 	github.com/morikuni/aec v1.0.0 // indirect | ||||||
|  | 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect | ||||||
|  | 	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect | ||||||
|  | 	go.opentelemetry.io/otel/sdk v1.34.0 // indirect | ||||||
|  | 	golang.org/x/term v0.28.0 // indirect | ||||||
|  | 	gotest.tools/v3 v3.5.2 // indirect | ||||||
|  | 	rsc.io/qr v0.2.0 // indirect | ||||||
|  | ) | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/Microsoft/go-winio v0.4.14 // indirect | 	github.com/Microsoft/go-winio v0.4.14 // indirect | ||||||
| 	github.com/atotto/clipboard v0.1.4 // indirect | 	github.com/atotto/clipboard v0.1.4 // indirect | ||||||
| 	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect | 	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect | ||||||
|  | 	github.com/boombuler/barcode v1.0.2 // indirect | ||||||
| 	github.com/bytedance/sonic v1.12.7 // indirect | 	github.com/bytedance/sonic v1.12.7 // indirect | ||||||
| 	github.com/bytedance/sonic/loader v0.2.3 // indirect | 	github.com/bytedance/sonic/loader v0.2.3 // indirect | ||||||
| 	github.com/catppuccin/go v0.2.0 // indirect | 	github.com/catppuccin/go v0.2.0 // indirect | ||||||
| 	github.com/charmbracelet/bubbles v0.20.0 // indirect | 	github.com/charmbracelet/bubbles v0.20.0 // indirect | ||||||
| 	github.com/charmbracelet/bubbletea v1.1.0 // indirect | 	github.com/charmbracelet/bubbletea v1.1.0 // indirect | ||||||
| 	github.com/charmbracelet/huh v0.6.0 // indirect | 	github.com/charmbracelet/huh v0.6.0 | ||||||
| 	github.com/charmbracelet/lipgloss v0.13.0 // indirect | 	github.com/charmbracelet/lipgloss v0.13.0 // indirect | ||||||
| 	github.com/charmbracelet/x/ansi v0.2.3 // indirect | 	github.com/charmbracelet/x/ansi v0.2.3 // indirect | ||||||
| 	github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect | 	github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect | ||||||
| 	github.com/charmbracelet/x/term v0.2.0 // indirect | 	github.com/charmbracelet/x/term v0.2.0 // indirect | ||||||
| 	github.com/cloudwego/base64x v0.1.4 // indirect | 	github.com/cloudwego/base64x v0.1.4 // indirect | ||||||
| 	github.com/distribution/reference v0.6.0 // indirect | 	github.com/distribution/reference v0.6.0 // indirect | ||||||
| 	github.com/docker/docker v27.5.1+incompatible // indirect | 	github.com/docker/docker v27.5.1+incompatible | ||||||
| 	github.com/docker/go-connections v0.5.0 // indirect | 	github.com/docker/go-connections v0.5.0 // indirect | ||||||
| 	github.com/docker/go-units v0.5.0 // indirect | 	github.com/docker/go-units v0.5.0 // indirect | ||||||
| 	github.com/dustin/go-humanize v1.0.1 // indirect | 	github.com/dustin/go-humanize v1.0.1 // indirect | ||||||
| @@ -38,7 +52,7 @@ require ( | |||||||
| 	github.com/fsnotify/fsnotify v1.7.0 // indirect | 	github.com/fsnotify/fsnotify v1.7.0 // indirect | ||||||
| 	github.com/gabriel-vasile/mimetype v1.4.8 // indirect | 	github.com/gabriel-vasile/mimetype v1.4.8 // indirect | ||||||
| 	github.com/gin-contrib/sse v1.0.0 // indirect | 	github.com/gin-contrib/sse v1.0.0 // indirect | ||||||
| 	github.com/go-logr/logr v1.4.1 // indirect | 	github.com/go-logr/logr v1.4.2 // indirect | ||||||
| 	github.com/go-logr/stdr v1.2.2 // indirect | 	github.com/go-logr/stdr v1.2.2 // indirect | ||||||
| 	github.com/go-playground/locales v0.14.1 // indirect | 	github.com/go-playground/locales v0.14.1 // indirect | ||||||
| 	github.com/go-playground/universal-translator v0.18.1 // indirect | 	github.com/go-playground/universal-translator v0.18.1 // indirect | ||||||
| @@ -53,7 +67,7 @@ require ( | |||||||
| 	github.com/klauspost/cpuid/v2 v2.2.9 // indirect | 	github.com/klauspost/cpuid/v2 v2.2.9 // indirect | ||||||
| 	github.com/leodido/go-urn v1.4.0 // indirect | 	github.com/leodido/go-urn v1.4.0 // indirect | ||||||
| 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect | 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect | ||||||
| 	github.com/magiconair/properties v1.8.7 // indirect | 	github.com/magiconair/properties v1.8.7 | ||||||
| 	github.com/mattn/go-colorable v0.1.14 // indirect | 	github.com/mattn/go-colorable v0.1.14 // indirect | ||||||
| 	github.com/mattn/go-isatty v0.0.20 // indirect | 	github.com/mattn/go-isatty v0.0.20 // indirect | ||||||
| 	github.com/mattn/go-localereader v0.0.1 // indirect | 	github.com/mattn/go-localereader v0.0.1 // indirect | ||||||
| @@ -70,6 +84,7 @@ require ( | |||||||
| 	github.com/opencontainers/image-spec v1.1.0 // indirect | 	github.com/opencontainers/image-spec v1.1.0 // indirect | ||||||
| 	github.com/pelletier/go-toml/v2 v2.2.3 // indirect | 	github.com/pelletier/go-toml/v2 v2.2.3 // indirect | ||||||
| 	github.com/pkg/errors v0.9.1 // indirect | 	github.com/pkg/errors v0.9.1 // indirect | ||||||
|  | 	github.com/pquerna/otp v1.4.0 | ||||||
| 	github.com/rivo/uniseg v0.4.7 // indirect | 	github.com/rivo/uniseg v0.4.7 // indirect | ||||||
| 	github.com/sagikazarmark/locafero v0.4.0 // indirect | 	github.com/sagikazarmark/locafero v0.4.0 // indirect | ||||||
| 	github.com/sagikazarmark/slog-shim v0.1.0 // indirect | 	github.com/sagikazarmark/slog-shim v0.1.0 // indirect | ||||||
| @@ -81,15 +96,15 @@ require ( | |||||||
| 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect | 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect | ||||||
| 	github.com/ugorji/go/codec v1.2.12 // indirect | 	github.com/ugorji/go/codec v1.2.12 // indirect | ||||||
| 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect | 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect | ||||||
| 	go.opentelemetry.io/otel v1.24.0 // indirect | 	go.opentelemetry.io/otel v1.34.0 // indirect | ||||||
| 	go.opentelemetry.io/otel/metric v1.24.0 // indirect | 	go.opentelemetry.io/otel/metric v1.34.0 // indirect | ||||||
| 	go.opentelemetry.io/otel/trace v1.24.0 // indirect | 	go.opentelemetry.io/otel/trace v1.34.0 // indirect | ||||||
| 	go.uber.org/atomic v1.9.0 // indirect | 	go.uber.org/atomic v1.9.0 // indirect | ||||||
| 	go.uber.org/multierr v1.9.0 // indirect | 	go.uber.org/multierr v1.9.0 // indirect | ||||||
| 	golang.org/x/arch v0.13.0 // indirect | 	golang.org/x/arch v0.13.0 // indirect | ||||||
| 	golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect | 	golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect | ||||||
| 	golang.org/x/net v0.34.0 // indirect | 	golang.org/x/net v0.34.0 // indirect | ||||||
| 	golang.org/x/oauth2 v0.25.0 // indirect | 	golang.org/x/oauth2 v0.25.0 | ||||||
| 	golang.org/x/sync v0.10.0 // indirect | 	golang.org/x/sync v0.10.0 // indirect | ||||||
| 	golang.org/x/sys v0.29.0 // indirect | 	golang.org/x/sys v0.29.0 // indirect | ||||||
| 	golang.org/x/text v0.21.0 // indirect | 	golang.org/x/text v0.21.0 // indirect | ||||||
|   | |||||||
							
								
								
									
										78
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										78
									
								
								go.sum
									
									
									
									
									
								
							| @@ -1,9 +1,16 @@ | |||||||
|  | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= | ||||||
|  | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= | ||||||
|  | github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= | ||||||
|  | github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= | ||||||
| github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= | github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= | ||||||
| github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= | github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= | ||||||
| github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= | ||||||
| github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= | ||||||
| github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= | ||||||
| github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= | ||||||
|  | github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= | ||||||
|  | github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4= | ||||||
|  | github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= | ||||||
| github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q= | github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q= | ||||||
| github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I= | github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I= | ||||||
| github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= | ||||||
| @@ -11,6 +18,8 @@ github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wio | |||||||
| github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= | github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= | ||||||
| github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= | github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= | ||||||
| github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= | github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= | ||||||
|  | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= | ||||||
|  | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= | ||||||
| github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= | github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= | ||||||
| github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= | github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= | ||||||
| github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c= | github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c= | ||||||
| @@ -28,6 +37,8 @@ github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4c | |||||||
| github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= | ||||||
| github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= | ||||||
| github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= | ||||||
|  | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= | ||||||
|  | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= | ||||||
| github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= | ||||||
| github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= | ||||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||||
| @@ -61,8 +72,8 @@ github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9S | |||||||
| github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= | ||||||
| github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= | ||||||
| github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= | ||||||
| github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= | ||||||
| github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= | ||||||
| github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= | ||||||
| github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= | ||||||
| github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= | ||||||
| @@ -79,19 +90,23 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x | |||||||
| github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= | ||||||
| github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= | ||||||
| github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||||
| github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= | ||||||
| github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | ||||||
| github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= | ||||||
| github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= | ||||||
| github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||||
| github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= | ||||||
| github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||||
|  | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | ||||||
|  | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||||
| github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= | github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= | ||||||
| github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= | github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= | ||||||
| github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= | github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= | ||||||
| github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= | github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= | ||||||
| github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= | github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= | ||||||
| github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= | github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= | ||||||
|  | github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= | ||||||
|  | github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= | ||||||
| github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= | ||||||
| github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= | ||||||
| github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= | ||||||
| @@ -126,17 +141,23 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J | |||||||
| github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= | ||||||
| github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= | ||||||
| github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= | ||||||
|  | github.com/mdp/qrterminal/v3 v3.2.0 h1:qteQMXO3oyTK4IHwj2mWsKYYRBOp1Pj2WRYFYYNTCdk= | ||||||
|  | github.com/mdp/qrterminal/v3 v3.2.0/go.mod h1:XGGuua4Lefrl7TLEsSONiD+UEjQXJZ4mPzF+gWYIJkk= | ||||||
| github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= | github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= | ||||||
| github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= | github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= | ||||||
| github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= | ||||||
| github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= | ||||||
| github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= | ||||||
| github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= | ||||||
|  | github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= | ||||||
|  | github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= | ||||||
| github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||||
| github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= | ||||||
| github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||||
| github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= | ||||||
| github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= | ||||||
|  | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= | ||||||
|  | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= | ||||||
| github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= | ||||||
| github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= | ||||||
| github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= | ||||||
| @@ -155,11 +176,13 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE | |||||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||||
| github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= | ||||||
| github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||||
|  | github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= | ||||||
|  | github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= | ||||||
| github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= | ||||||
| github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= | ||||||
| github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= | ||||||
| github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= | ||||||
| github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= | ||||||
| github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= | ||||||
| github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= | github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= | ||||||
| github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= | github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= | ||||||
| @@ -169,6 +192,8 @@ github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgY | |||||||
| github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= | ||||||
| github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= | ||||||
| github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= | ||||||
|  | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= | ||||||
|  | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= | ||||||
| github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= | ||||||
| github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= | ||||||
| github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= | ||||||
| @@ -203,14 +228,24 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E | |||||||
| github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= | ||||||
| github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | ||||||
| github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | ||||||
|  | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= | ||||||
|  | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= | ||||||
| go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= | ||||||
| go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= | ||||||
| go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= | go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= | ||||||
| go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= | go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= | ||||||
| go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= | ||||||
| go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= | ||||||
| go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs= | ||||||
| go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4= | ||||||
|  | go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= | ||||||
|  | go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= | ||||||
|  | go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= | ||||||
|  | go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= | ||||||
|  | go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= | ||||||
|  | go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= | ||||||
|  | go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= | ||||||
|  | go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= | ||||||
| go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= | ||||||
| go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= | ||||||
| go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= | go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= | ||||||
| @@ -250,10 +285,14 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |||||||
| golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= | ||||||
| golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||||
|  | golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= | ||||||
|  | golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= | ||||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||||
| golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= | ||||||
| golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= | ||||||
|  | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= | ||||||
|  | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= | ||||||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||||
| golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||||||
| golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= | ||||||
| @@ -262,14 +301,25 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T | |||||||
| golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||||
| golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||||
|  | google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= | ||||||
|  | google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= | ||||||
|  | google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= | ||||||
|  | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= | ||||||
|  | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= | ||||||
|  | google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= | ||||||
|  | google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= | ||||||
| google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= | google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= | ||||||
| google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= | google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= | ||||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||||
| gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | ||||||
| gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= | ||||||
| gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= | ||||||
| gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | ||||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||||
|  | gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= | ||||||
|  | gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= | ||||||
| nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= | ||||||
|  | rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= | ||||||
|  | rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= | ||||||
|   | |||||||
| @@ -19,6 +19,7 @@ import ( | |||||||
| 	"github.com/gin-contrib/sessions/cookie" | 	"github.com/gin-contrib/sessions/cookie" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/google/go-querystring/query" | 	"github.com/google/go-querystring/query" | ||||||
|  | 	"github.com/pquerna/otp/totp" | ||||||
| 	"github.com/rs/zerolog/log" | 	"github.com/rs/zerolog/log" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -84,7 +85,7 @@ func (api *API) Init() { | |||||||
| 		Path:     "/", | 		Path:     "/", | ||||||
| 		HttpOnly: true, | 		HttpOnly: true, | ||||||
| 		Secure:   api.Config.CookieSecure, | 		Secure:   api.Config.CookieSecure, | ||||||
| 		MaxAge:   api.Config.CookieExpiry, | 		MaxAge:   api.Config.SessionExpiry, | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	router.Use(sessions.Sessions("tinyauth", store)) | 	router.Use(sessions.Sessions("tinyauth", store)) | ||||||
| @@ -132,12 +133,45 @@ func (api *API) SetupRoutes() { | |||||||
|  |  | ||||||
| 		log.Debug().Interface("proxy", proxy.Proxy).Msg("Got proxy") | 		log.Debug().Interface("proxy", proxy.Proxy).Msg("Got proxy") | ||||||
|  |  | ||||||
| 		// Get user context |  | ||||||
| 		userContext := api.Hooks.UseUserContext(c) |  | ||||||
|  |  | ||||||
| 		// Check if using basic auth | 		// Check if using basic auth | ||||||
| 		_, _, basicAuth := c.Request.BasicAuth() | 		_, _, basicAuth := c.Request.BasicAuth() | ||||||
|  |  | ||||||
|  | 		// Check if auth is enabled | ||||||
|  | 		authEnabled, authEnabledErr := api.Auth.AuthEnabled(c) | ||||||
|  |  | ||||||
|  | 		// Handle error | ||||||
|  | 		if authEnabledErr != nil { | ||||||
|  | 			// Return 500 if nginx is the proxy or if the request is using basic auth | ||||||
|  | 			if proxy.Proxy == "nginx" || basicAuth { | ||||||
|  | 				log.Error().Err(authEnabledErr).Msg("Failed to check if auth is enabled") | ||||||
|  | 				c.JSON(500, gin.H{ | ||||||
|  | 					"status":  500, | ||||||
|  | 					"message": "Internal Server Error", | ||||||
|  | 				}) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Return the internal server error page | ||||||
|  | 			if api.handleError(c, "Failed to check if auth is enabled", authEnabledErr) { | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// If auth is not enabled, return 200 | ||||||
|  | 		if !authEnabled { | ||||||
|  | 			// The user is allowed to access the app | ||||||
|  | 			c.JSON(200, gin.H{ | ||||||
|  | 				"status":  200, | ||||||
|  | 				"message": "Authenticated", | ||||||
|  | 			}) | ||||||
|  |  | ||||||
|  | 			// Stop further processing | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Get user context | ||||||
|  | 		userContext := api.Hooks.UseUserContext(c) | ||||||
|  |  | ||||||
| 		// Get headers | 		// Get headers | ||||||
| 		uri := c.Request.Header.Get("X-Forwarded-Uri") | 		uri := c.Request.Header.Get("X-Forwarded-Uri") | ||||||
| 		proto := c.Request.Header.Get("X-Forwarded-Proto") | 		proto := c.Request.Header.Get("X-Forwarded-Proto") | ||||||
| @@ -148,15 +182,15 @@ func (api *API) SetupRoutes() { | |||||||
| 			log.Debug().Msg("Authenticated") | 			log.Debug().Msg("Authenticated") | ||||||
|  |  | ||||||
| 			// Check if user is allowed to access subdomain, if request is nginx.example.com the subdomain (resource) is nginx | 			// Check if user is allowed to access subdomain, if request is nginx.example.com the subdomain (resource) is nginx | ||||||
| 			appAllowed, appAllowedErr := api.Auth.ResourceAllowed(userContext, host) | 			appAllowed, appAllowedErr := api.Auth.ResourceAllowed(c, userContext) | ||||||
|  |  | ||||||
| 			// Check if there was an error | 			// Check if there was an error | ||||||
| 			if appAllowedErr != nil { | 			if appAllowedErr != nil { | ||||||
| 				// Return 501 if nginx is the proxy or if the request is using basic auth | 				// Return 500 if nginx is the proxy or if the request is using basic auth | ||||||
| 				if proxy.Proxy == "nginx" || basicAuth { | 				if proxy.Proxy == "nginx" || basicAuth { | ||||||
| 					log.Error().Err(appAllowedErr).Msg("Failed to check if app is allowed") | 					log.Error().Err(appAllowedErr).Msg("Failed to check if app is allowed") | ||||||
| 					c.JSON(501, gin.H{ | 					c.JSON(500, gin.H{ | ||||||
| 						"status":  501, | 						"status":  500, | ||||||
| 						"message": "Internal Server Error", | 						"message": "Internal Server Error", | ||||||
| 					}) | 					}) | ||||||
| 					return | 					return | ||||||
| @@ -202,6 +236,9 @@ func (api *API) SetupRoutes() { | |||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|  | 			// Set the user header | ||||||
|  | 			c.Header("Remote-User", userContext.Username) | ||||||
|  |  | ||||||
| 			// The user is allowed to access the app | 			// The user is allowed to access the app | ||||||
| 			c.JSON(200, gin.H{ | 			c.JSON(200, gin.H{ | ||||||
| 				"status":  200, | 				"status":  200, | ||||||
| @@ -285,7 +322,29 @@ func (api *API) SetupRoutes() { | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		log.Debug().Msg("Password correct, logging in") | 		log.Debug().Msg("Password correct, checking totp") | ||||||
|  |  | ||||||
|  | 		// Check if user has totp enabled | ||||||
|  | 		if user.TotpSecret != "" { | ||||||
|  | 			log.Debug().Msg("Totp enabled") | ||||||
|  |  | ||||||
|  | 			// Set totp pending cookie | ||||||
|  | 			api.Auth.CreateSessionCookie(c, &types.SessionCookie{ | ||||||
|  | 				Username:    login.Username, | ||||||
|  | 				Provider:    "username", | ||||||
|  | 				TotpPending: true, | ||||||
|  | 			}) | ||||||
|  |  | ||||||
|  | 			// Return totp required | ||||||
|  | 			c.JSON(200, gin.H{ | ||||||
|  | 				"status":      200, | ||||||
|  | 				"message":     "Waiting for totp", | ||||||
|  | 				"totpPending": true, | ||||||
|  | 			}) | ||||||
|  |  | ||||||
|  | 			// Stop further processing | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		// Create session cookie with username as provider | 		// Create session cookie with username as provider | ||||||
| 		api.Auth.CreateSessionCookie(c, &types.SessionCookie{ | 		api.Auth.CreateSessionCookie(c, &types.SessionCookie{ | ||||||
| @@ -293,6 +352,80 @@ func (api *API) SetupRoutes() { | |||||||
| 			Provider: "username", | 			Provider: "username", | ||||||
| 		}) | 		}) | ||||||
|  |  | ||||||
|  | 		// Return logged in | ||||||
|  | 		c.JSON(200, gin.H{ | ||||||
|  | 			"status":      200, | ||||||
|  | 			"message":     "Logged in", | ||||||
|  | 			"totpPending": false, | ||||||
|  | 		}) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	api.Router.POST("/api/totp", func(c *gin.Context) { | ||||||
|  | 		// Create totp struct | ||||||
|  | 		var totpReq types.Totp | ||||||
|  |  | ||||||
|  | 		// Bind JSON | ||||||
|  | 		err := c.BindJSON(&totpReq) | ||||||
|  |  | ||||||
|  | 		// Handle error | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Error().Err(err).Msg("Failed to bind JSON") | ||||||
|  | 			c.JSON(400, gin.H{ | ||||||
|  | 				"status":  400, | ||||||
|  | 				"message": "Bad Request", | ||||||
|  | 			}) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		log.Debug().Msg("Checking totp") | ||||||
|  |  | ||||||
|  | 		// Get user context | ||||||
|  | 		userContext := api.Hooks.UseUserContext(c) | ||||||
|  |  | ||||||
|  | 		// Check if we have a user | ||||||
|  | 		if userContext.Username == "" { | ||||||
|  | 			log.Debug().Msg("No user context") | ||||||
|  | 			c.JSON(401, gin.H{ | ||||||
|  | 				"status":  401, | ||||||
|  | 				"message": "Unauthorized", | ||||||
|  | 			}) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Get user | ||||||
|  | 		user := api.Auth.GetUser(userContext.Username) | ||||||
|  |  | ||||||
|  | 		// Check if user exists | ||||||
|  | 		if user == nil { | ||||||
|  | 			log.Debug().Msg("User not found") | ||||||
|  | 			c.JSON(401, gin.H{ | ||||||
|  | 				"status":  401, | ||||||
|  | 				"message": "Unauthorized", | ||||||
|  | 			}) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Check if totp is correct | ||||||
|  | 		totpOk := totp.Validate(totpReq.Code, user.TotpSecret) | ||||||
|  |  | ||||||
|  | 		// TOTP is incorrect | ||||||
|  | 		if !totpOk { | ||||||
|  | 			log.Debug().Msg("Totp incorrect") | ||||||
|  | 			c.JSON(401, gin.H{ | ||||||
|  | 				"status":  401, | ||||||
|  | 				"message": "Unauthorized", | ||||||
|  | 			}) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		log.Debug().Msg("Totp correct") | ||||||
|  |  | ||||||
|  | 		// Create session cookie with username as provider | ||||||
|  | 		api.Auth.CreateSessionCookie(c, &types.SessionCookie{ | ||||||
|  | 			Username: user.Username, | ||||||
|  | 			Provider: "username", | ||||||
|  | 		}) | ||||||
|  |  | ||||||
| 		// Return logged in | 		// Return logged in | ||||||
| 		c.JSON(200, gin.H{ | 		c.JSON(200, gin.H{ | ||||||
| 			"status":  200, | 			"status":  200, | ||||||
| @@ -332,36 +465,33 @@ func (api *API) SetupRoutes() { | |||||||
| 			configuredProviders = append(configuredProviders, "username") | 			configuredProviders = append(configuredProviders, "username") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// We are not logged in so return unauthorized | 		// Fill status struct with data from user context and api config | ||||||
|  | 		status := types.Status{ | ||||||
|  | 			Username:            userContext.Username, | ||||||
|  | 			IsLoggedIn:          userContext.IsLoggedIn, | ||||||
|  | 			Oauth:               userContext.OAuth, | ||||||
|  | 			Provider:            userContext.Provider, | ||||||
|  | 			ConfiguredProviders: configuredProviders, | ||||||
|  | 			DisableContinue:     api.Config.DisableContinue, | ||||||
|  | 			Title:               api.Config.Title, | ||||||
|  | 			GenericName:         api.Config.GenericName, | ||||||
|  | 			TotpPending:         userContext.TotpPending, | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// If we are not logged in we set the status to 401 and add the WWW-Authenticate header else we set it to 200 | ||||||
| 		if !userContext.IsLoggedIn { | 		if !userContext.IsLoggedIn { | ||||||
| 			log.Debug().Msg("Unauthorized") | 			log.Debug().Msg("Unauthorized") | ||||||
| 			c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"") | 			c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"") | ||||||
| 			c.JSON(200, gin.H{ | 			status.Status = 401 | ||||||
| 				"status":              200, | 			status.Message = "Unauthorized" | ||||||
| 				"message":             "Unauthorized", | 		} else { | ||||||
| 				"username":            "", | 			log.Debug().Interface("userContext", userContext).Strs("configuredProviders", configuredProviders).Bool("disableContinue", api.Config.DisableContinue).Msg("Authenticated") | ||||||
| 				"isLoggedIn":          false, | 			status.Status = 200 | ||||||
| 				"oauth":               false, | 			status.Message = "Authenticated" | ||||||
| 				"provider":            "", |  | ||||||
| 				"configuredProviders": configuredProviders, |  | ||||||
| 				"disableContinue":     api.Config.DisableContinue, |  | ||||||
| 			}) |  | ||||||
| 			return |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		log.Debug().Interface("userContext", userContext).Strs("configuredProviders", configuredProviders).Bool("disableContinue", api.Config.DisableContinue).Msg("Authenticated") | 		// Return data | ||||||
|  | 		c.JSON(200, status) | ||||||
| 		// We are logged in so return our user context |  | ||||||
| 		c.JSON(200, gin.H{ |  | ||||||
| 			"status":              200, |  | ||||||
| 			"message":             "Authenticated", |  | ||||||
| 			"username":            userContext.Username, |  | ||||||
| 			"isLoggedIn":          userContext.IsLoggedIn, |  | ||||||
| 			"oauth":               userContext.OAuth, |  | ||||||
| 			"provider":            userContext.Provider, |  | ||||||
| 			"configuredProviders": configuredProviders, |  | ||||||
| 			"disableContinue":     api.Config.DisableContinue, |  | ||||||
| 		}) |  | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	api.Router.GET("/api/oauth/url/:provider", func(c *gin.Context) { | 	api.Router.GET("/api/oauth/url/:provider", func(c *gin.Context) { | ||||||
|   | |||||||
| @@ -23,7 +23,7 @@ var apiConfig = types.APIConfig{ | |||||||
| 	Secret:          "super-secret-api-thing-for-tests", // It is 32 chars long | 	Secret:          "super-secret-api-thing-for-tests", // It is 32 chars long | ||||||
| 	AppURL:          "http://tinyauth.localhost", | 	AppURL:          "http://tinyauth.localhost", | ||||||
| 	CookieSecure:    false, | 	CookieSecure:    false, | ||||||
| 	CookieExpiry:    3600, | 	SessionExpiry:   3600, | ||||||
| 	DisableContinue: false, | 	DisableContinue: false, | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -55,7 +55,7 @@ func getAPI(t *testing.T) *api.API { | |||||||
| 			Username: user.Username, | 			Username: user.Username, | ||||||
| 			Password: user.Password, | 			Password: user.Password, | ||||||
| 		}, | 		}, | ||||||
| 	}, nil, apiConfig.CookieExpiry) | 	}, nil, apiConfig.SessionExpiry) | ||||||
|  |  | ||||||
| 	// Create providers service | 	// Create providers service | ||||||
| 	providers := providers.NewProviders(types.OAuthConfig{}) | 	providers := providers.NewProviders(types.OAuthConfig{}) | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| v3.0.0 | v3.1.0 | ||||||
| @@ -1,12 +1,12 @@ | |||||||
| package auth | package auth | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"regexp" | ||||||
| 	"slices" | 	"slices" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| 	"tinyauth/internal/docker" | 	"tinyauth/internal/docker" | ||||||
| 	"tinyauth/internal/types" | 	"tinyauth/internal/types" | ||||||
| 	"tinyauth/internal/utils" |  | ||||||
|  |  | ||||||
| 	"github.com/gin-contrib/sessions" | 	"github.com/gin-contrib/sessions" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| @@ -70,10 +70,20 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) | |||||||
|  |  | ||||||
| 	log.Debug().Msg("Setting session cookie") | 	log.Debug().Msg("Setting session cookie") | ||||||
|  |  | ||||||
|  | 	// Calculate expiry | ||||||
|  | 	var sessionExpiry int | ||||||
|  |  | ||||||
|  | 	if data.TotpPending { | ||||||
|  | 		sessionExpiry = 3600 | ||||||
|  | 	} else { | ||||||
|  | 		sessionExpiry = auth.SessionExpiry | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Set data | 	// Set data | ||||||
| 	sessions.Set("username", data.Username) | 	sessions.Set("username", data.Username) | ||||||
| 	sessions.Set("provider", data.Provider) | 	sessions.Set("provider", data.Provider) | ||||||
| 	sessions.Set("expiry", time.Now().Add(time.Duration(auth.SessionExpiry)*time.Second).Unix()) | 	sessions.Set("expiry", time.Now().Add(time.Duration(sessionExpiry)*time.Second).Unix()) | ||||||
|  | 	sessions.Set("totpPending", data.TotpPending) | ||||||
|  |  | ||||||
| 	// Save session | 	// Save session | ||||||
| 	sessions.Save() | 	sessions.Save() | ||||||
| @@ -102,14 +112,16 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) types.SessionCookie { | |||||||
| 	cookieUsername := sessions.Get("username") | 	cookieUsername := sessions.Get("username") | ||||||
| 	cookieProvider := sessions.Get("provider") | 	cookieProvider := sessions.Get("provider") | ||||||
| 	cookieExpiry := sessions.Get("expiry") | 	cookieExpiry := sessions.Get("expiry") | ||||||
|  | 	cookieTotpPending := sessions.Get("totpPending") | ||||||
|  |  | ||||||
| 	// Convert interfaces to correct types | 	// Convert interfaces to correct types | ||||||
| 	username, usernameOk := cookieUsername.(string) | 	username, usernameOk := cookieUsername.(string) | ||||||
| 	provider, providerOk := cookieProvider.(string) | 	provider, providerOk := cookieProvider.(string) | ||||||
| 	expiry, expiryOk := cookieExpiry.(int64) | 	expiry, expiryOk := cookieExpiry.(int64) | ||||||
|  | 	totpPending, totpPendingOk := cookieTotpPending.(bool) | ||||||
|  |  | ||||||
| 	// Check if the cookie is invalid | 	// Check if the cookie is invalid | ||||||
| 	if !usernameOk || !providerOk || !expiryOk { | 	if !usernameOk || !providerOk || !expiryOk || !totpPendingOk { | ||||||
| 		log.Warn().Msg("Session cookie invalid") | 		log.Warn().Msg("Session cookie invalid") | ||||||
| 		return types.SessionCookie{} | 		return types.SessionCookie{} | ||||||
| 	} | 	} | ||||||
| @@ -125,12 +137,13 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) types.SessionCookie { | |||||||
| 		return types.SessionCookie{} | 		return types.SessionCookie{} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Debug().Str("username", username).Str("provider", provider).Int64("expiry", expiry).Msg("Parsed cookie") | 	log.Debug().Str("username", username).Str("provider", provider).Int64("expiry", expiry).Bool("totpPending", totpPending).Msg("Parsed cookie") | ||||||
|  |  | ||||||
| 	// Return the cookie | 	// Return the cookie | ||||||
| 	return types.SessionCookie{ | 	return types.SessionCookie{ | ||||||
| 		Username:    username, | 		Username:    username, | ||||||
| 		Provider:    provider, | 		Provider:    provider, | ||||||
|  | 		TotpPending: totpPending, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -139,51 +152,15 @@ func (auth *Auth) UserAuthConfigured() bool { | |||||||
| 	return len(auth.Users) > 0 | 	return len(auth.Users) > 0 | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *Auth) ResourceAllowed(context types.UserContext, host string) (bool, error) { | func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext) (bool, error) { | ||||||
| 	// Check if we have access to the Docker API | 	// Get headers | ||||||
| 	isConnected := auth.Docker.DockerConnected() | 	host := c.Request.Header.Get("X-Forwarded-Host") | ||||||
|  |  | ||||||
| 	// If we don't have access, it is assumed that the user has access | 	// Get app id | ||||||
| 	if !isConnected { |  | ||||||
| 		log.Debug().Msg("Docker not connected, allowing access") |  | ||||||
| 		return true, nil |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Get the app ID from the host |  | ||||||
| 	appId := strings.Split(host, ".")[0] | 	appId := strings.Split(host, ".")[0] | ||||||
|  |  | ||||||
| 	// Get the containers | 	// Check if resource is allowed | ||||||
| 	containers, containersErr := auth.Docker.GetContainers() | 	allowed, allowedErr := auth.Docker.ContainerAction(appId, func(labels types.TinyauthLabels) (bool, error) { | ||||||
|  |  | ||||||
| 	// If there is an error, return false |  | ||||||
| 	if containersErr != nil { |  | ||||||
| 		return false, containersErr |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Got containers") |  | ||||||
|  |  | ||||||
| 	// Loop through the containers |  | ||||||
| 	for _, container := range containers { |  | ||||||
| 		// Inspect the container |  | ||||||
| 		inspect, inspectErr := auth.Docker.InspectContainer(container.ID) |  | ||||||
|  |  | ||||||
| 		// If there is an error, return false |  | ||||||
| 		if inspectErr != nil { |  | ||||||
| 			return false, inspectErr |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// Get the container name (for some reason it is /name) |  | ||||||
| 		containerName := strings.Split(inspect.Name, "/")[1] |  | ||||||
|  |  | ||||||
| 		// There is a container with the same name as the app ID |  | ||||||
| 		if containerName == appId { |  | ||||||
| 			log.Debug().Str("container", containerName).Msg("Found container") |  | ||||||
|  |  | ||||||
| 			// Get only the tinyauth labels in a struct |  | ||||||
| 			labels := utils.GetTinyauthLabels(inspect.Config.Labels) |  | ||||||
|  |  | ||||||
| 			log.Debug().Msg("Got labels") |  | ||||||
|  |  | ||||||
| 		// If the container has an oauth whitelist, check if the user is in it | 		// If the container has an oauth whitelist, check if the user is in it | ||||||
| 		if context.OAuth && len(labels.OAuthWhitelist) != 0 { | 		if context.OAuth && len(labels.OAuthWhitelist) != 0 { | ||||||
| 			log.Debug().Msg("Checking OAuth whitelist") | 			log.Debug().Msg("Checking OAuth whitelist") | ||||||
| @@ -201,14 +178,63 @@ func (auth *Auth) ResourceAllowed(context types.UserContext, host string) (bool, | |||||||
| 			} | 			} | ||||||
| 			return false, nil | 			return false, nil | ||||||
| 		} | 		} | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 	} | 		// Allowed | ||||||
|  |  | ||||||
| 	log.Debug().Msg("No matching container found, allowing access") |  | ||||||
|  |  | ||||||
| 	// If no matching container is found, allow access |  | ||||||
| 		return true, nil | 		return true, nil | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	// If there is an error, return false | ||||||
|  | 	if allowedErr != nil { | ||||||
|  | 		log.Error().Err(allowedErr).Msg("Error checking if resource is allowed") | ||||||
|  | 		return false, allowedErr | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Return if the resource is allowed | ||||||
|  | 	return allowed, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (auth *Auth) AuthEnabled(c *gin.Context) (bool, error) { | ||||||
|  | 	// Get headers | ||||||
|  | 	uri := c.Request.Header.Get("X-Forwarded-Uri") | ||||||
|  | 	host := c.Request.Header.Get("X-Forwarded-Host") | ||||||
|  |  | ||||||
|  | 	// Get app id | ||||||
|  | 	appId := strings.Split(host, ".")[0] | ||||||
|  |  | ||||||
|  | 	// Check if auth is enabled | ||||||
|  | 	enabled, enabledErr := auth.Docker.ContainerAction(appId, func(labels types.TinyauthLabels) (bool, error) { | ||||||
|  | 		// Check if the allowed label is empty | ||||||
|  | 		if labels.Allowed == "" { | ||||||
|  | 			// Auth enabled | ||||||
|  | 			return true, nil | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Compile regex | ||||||
|  | 		regex, regexErr := regexp.Compile(labels.Allowed) | ||||||
|  |  | ||||||
|  | 		// If there is an error, invalid regex, auth enabled | ||||||
|  | 		if regexErr != nil { | ||||||
|  | 			log.Warn().Err(regexErr).Msg("Invalid regex") | ||||||
|  | 			return true, regexErr | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Check if the uri matches the regex | ||||||
|  | 		if regex.MatchString(uri) { | ||||||
|  | 			// Auth disabled | ||||||
|  | 			return false, nil | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Auth enabled | ||||||
|  | 		return true, nil | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	// If there is an error, auth enabled | ||||||
|  | 	if enabledErr != nil { | ||||||
|  | 		log.Error().Err(enabledErr).Msg("Error checking if auth is enabled") | ||||||
|  | 		return true, enabledErr | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return enabled, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *Auth) GetBasicAuth(c *gin.Context) *types.User { | func (auth *Auth) GetBasicAuth(c *gin.Context) *types.User { | ||||||
|   | |||||||
| @@ -4,4 +4,5 @@ package constants | |||||||
| var TinyauthLabels = []string{ | var TinyauthLabels = []string{ | ||||||
| 	"tinyauth.oauth.whitelist", | 	"tinyauth.oauth.whitelist", | ||||||
| 	"tinyauth.users", | 	"tinyauth.users", | ||||||
|  | 	"tinyauth.allowed", | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,10 +2,14 @@ package docker | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | 	"strings" | ||||||
|  | 	appTypes "tinyauth/internal/types" | ||||||
|  | 	"tinyauth/internal/utils" | ||||||
|  |  | ||||||
| 	"github.com/docker/docker/api/types" | 	apiTypes "github.com/docker/docker/api/types" | ||||||
| 	"github.com/docker/docker/api/types/container" | 	containerTypes "github.com/docker/docker/api/types/container" | ||||||
| 	"github.com/docker/docker/client" | 	"github.com/docker/docker/client" | ||||||
|  | 	"github.com/rs/zerolog/log" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func NewDocker() *Docker { | func NewDocker() *Docker { | ||||||
| @@ -34,9 +38,9 @@ func (docker *Docker) Init() error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (docker *Docker) GetContainers() ([]types.Container, error) { | func (docker *Docker) GetContainers() ([]apiTypes.Container, error) { | ||||||
| 	// Get the list of containers | 	// Get the list of containers | ||||||
| 	containers, err := docker.Client.ContainerList(docker.Context, container.ListOptions{}) | 	containers, err := docker.Client.ContainerList(docker.Context, containerTypes.ListOptions{}) | ||||||
|  |  | ||||||
| 	// Check if there was an error | 	// Check if there was an error | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -47,13 +51,13 @@ func (docker *Docker) GetContainers() ([]types.Container, error) { | |||||||
| 	return containers, nil | 	return containers, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (docker *Docker) InspectContainer(containerId string) (types.ContainerJSON, error) { | func (docker *Docker) InspectContainer(containerId string) (apiTypes.ContainerJSON, error) { | ||||||
| 	// Inspect the container | 	// Inspect the container | ||||||
| 	inspect, err := docker.Client.ContainerInspect(docker.Context, containerId) | 	inspect, err := docker.Client.ContainerInspect(docker.Context, containerId) | ||||||
|  |  | ||||||
| 	// Check if there was an error | 	// Check if there was an error | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return types.ContainerJSON{}, err | 		return apiTypes.ContainerJSON{}, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Return the inspect | 	// Return the inspect | ||||||
| @@ -65,3 +69,57 @@ func (docker *Docker) DockerConnected() bool { | |||||||
| 	_, err := docker.Client.Ping(docker.Context) | 	_, err := docker.Client.Ping(docker.Context) | ||||||
| 	return err == nil | 	return err == nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (docker *Docker) ContainerAction(appId string, runCheck func(labels appTypes.TinyauthLabels) (bool, error)) (bool, error) { | ||||||
|  | 	// Check if we have access to the Docker API | ||||||
|  | 	isConnected := docker.DockerConnected() | ||||||
|  |  | ||||||
|  | 	// If we don't have access, it is assumed that the check passed | ||||||
|  | 	if !isConnected { | ||||||
|  | 		log.Debug().Msg("Docker not connected, passing check") | ||||||
|  | 		return true, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Get the containers | ||||||
|  | 	containers, containersErr := docker.GetContainers() | ||||||
|  |  | ||||||
|  | 	// If there is an error, return false | ||||||
|  | 	if containersErr != nil { | ||||||
|  | 		return false, containersErr | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log.Debug().Msg("Got containers") | ||||||
|  |  | ||||||
|  | 	// Loop through the containers | ||||||
|  | 	for _, container := range containers { | ||||||
|  | 		// Inspect the container | ||||||
|  | 		inspect, inspectErr := docker.InspectContainer(container.ID) | ||||||
|  |  | ||||||
|  | 		// If there is an error, return false | ||||||
|  | 		if inspectErr != nil { | ||||||
|  | 			return false, inspectErr | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Get the container name (for some reason it is /name) | ||||||
|  | 		containerName := strings.Split(inspect.Name, "/")[1] | ||||||
|  |  | ||||||
|  | 		// There is a container with the same name as the app ID | ||||||
|  | 		if containerName == appId { | ||||||
|  | 			log.Debug().Str("container", containerName).Msg("Found container") | ||||||
|  |  | ||||||
|  | 			// Get only the tinyauth labels in a struct | ||||||
|  | 			labels := utils.GetTinyauthLabels(inspect.Config.Labels) | ||||||
|  |  | ||||||
|  | 			log.Debug().Msg("Got labels") | ||||||
|  |  | ||||||
|  | 			// Run the check | ||||||
|  | 			return runCheck(labels) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log.Debug().Msg("No matching container found, passing check") | ||||||
|  |  | ||||||
|  | 	// If no matching container is found, pass check | ||||||
|  | 	return true, nil | ||||||
|  | } | ||||||
|   | |||||||
| @@ -40,11 +40,25 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { | |||||||
| 				IsLoggedIn:  true, | 				IsLoggedIn:  true, | ||||||
| 				OAuth:       false, | 				OAuth:       false, | ||||||
| 				Provider:    "basic", | 				Provider:    "basic", | ||||||
|  | 				TotpPending: false, | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// Check if session cookie has totp pending | ||||||
|  | 	if cookie.TotpPending { | ||||||
|  | 		log.Debug().Msg("Totp pending") | ||||||
|  | 		// Return empty context since we are pending totp | ||||||
|  | 		return types.UserContext{ | ||||||
|  | 			Username:    cookie.Username, | ||||||
|  | 			IsLoggedIn:  false, | ||||||
|  | 			OAuth:       false, | ||||||
|  | 			Provider:    cookie.Provider, | ||||||
|  | 			TotpPending: true, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Check if session cookie is username/password auth | 	// Check if session cookie is username/password auth | ||||||
| 	if cookie.Provider == "username" { | 	if cookie.Provider == "username" { | ||||||
| 		log.Debug().Msg("Provider is username") | 		log.Debug().Msg("Provider is username") | ||||||
| @@ -59,6 +73,7 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { | |||||||
| 				IsLoggedIn:  true, | 				IsLoggedIn:  true, | ||||||
| 				OAuth:       false, | 				OAuth:       false, | ||||||
| 				Provider:    "username", | 				Provider:    "username", | ||||||
|  | 				TotpPending: false, | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @@ -85,6 +100,7 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { | |||||||
| 				IsLoggedIn:  false, | 				IsLoggedIn:  false, | ||||||
| 				OAuth:       false, | 				OAuth:       false, | ||||||
| 				Provider:    "", | 				Provider:    "", | ||||||
|  | 				TotpPending: false, | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -96,6 +112,7 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { | |||||||
| 			IsLoggedIn:  true, | 			IsLoggedIn:  true, | ||||||
| 			OAuth:       true, | 			OAuth:       true, | ||||||
| 			Provider:    cookie.Provider, | 			Provider:    cookie.Provider, | ||||||
|  | 			TotpPending: false, | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -105,5 +122,6 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { | |||||||
| 		IsLoggedIn:  false, | 		IsLoggedIn:  false, | ||||||
| 		OAuth:       false, | 		OAuth:       false, | ||||||
| 		Provider:    "", | 		Provider:    "", | ||||||
|  | 		TotpPending: false, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -17,6 +17,7 @@ type LoginRequest struct { | |||||||
| type User struct { | type User struct { | ||||||
| 	Username   string | 	Username   string | ||||||
| 	Password   string | 	Password   string | ||||||
|  | 	TotpSecret string | ||||||
| } | } | ||||||
|  |  | ||||||
| // Users is a list of users | // Users is a list of users | ||||||
| @@ -48,10 +49,12 @@ type Config struct { | |||||||
| 	GenericAuthURL            string `mapstructure:"generic-auth-url"` | 	GenericAuthURL            string `mapstructure:"generic-auth-url"` | ||||||
| 	GenericTokenURL           string `mapstructure:"generic-token-url"` | 	GenericTokenURL           string `mapstructure:"generic-token-url"` | ||||||
| 	GenericUserURL            string `mapstructure:"generic-user-url"` | 	GenericUserURL            string `mapstructure:"generic-user-url"` | ||||||
|  | 	GenericName               string `mapstructure:"generic-name"` | ||||||
| 	DisableContinue           bool   `mapstructure:"disable-continue"` | 	DisableContinue           bool   `mapstructure:"disable-continue"` | ||||||
| 	OAuthWhitelist            string `mapstructure:"oauth-whitelist"` | 	OAuthWhitelist            string `mapstructure:"oauth-whitelist"` | ||||||
| 	SessionExpiry             int    `mapstructure:"session-expiry"` | 	SessionExpiry             int    `mapstructure:"session-expiry"` | ||||||
| 	LogLevel                  int8   `mapstructure:"log-level" validate:"min=-1,max=5"` | 	LogLevel                  int8   `mapstructure:"log-level" validate:"min=-1,max=5"` | ||||||
|  | 	Title                     string `mapstructure:"app-title"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // UserContext is the context for the user | // UserContext is the context for the user | ||||||
| @@ -60,6 +63,7 @@ type UserContext struct { | |||||||
| 	IsLoggedIn  bool | 	IsLoggedIn  bool | ||||||
| 	OAuth       bool | 	OAuth       bool | ||||||
| 	Provider    string | 	Provider    string | ||||||
|  | 	TotpPending bool | ||||||
| } | } | ||||||
|  |  | ||||||
| // APIConfig is the configuration for the API | // APIConfig is the configuration for the API | ||||||
| @@ -69,8 +73,10 @@ type APIConfig struct { | |||||||
| 	Secret          string | 	Secret          string | ||||||
| 	AppURL          string | 	AppURL          string | ||||||
| 	CookieSecure    bool | 	CookieSecure    bool | ||||||
| 	CookieExpiry    int | 	SessionExpiry   int | ||||||
| 	DisableContinue bool | 	DisableContinue bool | ||||||
|  | 	GenericName     string | ||||||
|  | 	Title           string | ||||||
| } | } | ||||||
|  |  | ||||||
| // OAuthConfig is the configuration for the providers | // OAuthConfig is the configuration for the providers | ||||||
| @@ -112,12 +118,14 @@ type UnauthorizedQuery struct { | |||||||
| type SessionCookie struct { | type SessionCookie struct { | ||||||
| 	Username    string | 	Username    string | ||||||
| 	Provider    string | 	Provider    string | ||||||
|  | 	TotpPending bool | ||||||
| } | } | ||||||
|  |  | ||||||
| // TinyauthLabels is the labels for the tinyauth container | // TinyauthLabels is the labels for the tinyauth container | ||||||
| type TinyauthLabels struct { | type TinyauthLabels struct { | ||||||
| 	OAuthWhitelist []string | 	OAuthWhitelist []string | ||||||
| 	Users          []string | 	Users          []string | ||||||
|  | 	Allowed        string | ||||||
| } | } | ||||||
|  |  | ||||||
| // TailscaleQuery is the query parameters for the tailscale endpoint | // TailscaleQuery is the query parameters for the tailscale endpoint | ||||||
| @@ -129,3 +137,23 @@ type TailscaleQuery struct { | |||||||
| type Proxy struct { | type Proxy struct { | ||||||
| 	Proxy string `uri:"proxy" binding:"required"` | 	Proxy string `uri:"proxy" binding:"required"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Status response | ||||||
|  | type Status struct { | ||||||
|  | 	Status              int      `json:"status"` | ||||||
|  | 	Message             string   `json:"message"` | ||||||
|  | 	IsLoggedIn          bool     `json:"isLoggedIn"` | ||||||
|  | 	Username            string   `json:"username"` | ||||||
|  | 	Provider            string   `json:"provider"` | ||||||
|  | 	Oauth               bool     `json:"oauth"` | ||||||
|  | 	ConfiguredProviders []string `json:"configuredProviders"` | ||||||
|  | 	DisableContinue     bool     `json:"disableContinue"` | ||||||
|  | 	Title               string   `json:"title"` | ||||||
|  | 	GenericName         string   `json:"genericName"` | ||||||
|  | 	TotpPending         bool     `json:"totpPending"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Totp request | ||||||
|  | type Totp struct { | ||||||
|  | 	Code string `json:"code"` | ||||||
|  | } | ||||||
|   | |||||||
| @@ -29,19 +29,15 @@ func ParseUsers(users string) (types.Users, error) { | |||||||
|  |  | ||||||
| 	// Loop through the users and split them by colon | 	// Loop through the users and split them by colon | ||||||
| 	for _, user := range userList { | 	for _, user := range userList { | ||||||
| 		// Split the user by colon | 		parsed, parseErr := ParseUser(user) | ||||||
| 		userSplit := strings.Split(user, ":") |  | ||||||
|  |  | ||||||
| 		// Check if the user is in the correct format | 		// Check if there was an error | ||||||
| 		if len(userSplit) != 2 { | 		if parseErr != nil { | ||||||
| 			return types.Users{}, errors.New("invalid user format") | 			return types.Users{}, parseErr | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Append the user to the users struct | 		// Append the user to the users struct | ||||||
| 		usersParsed = append(usersParsed, types.User{ | 		usersParsed = append(usersParsed, parsed) | ||||||
| 			Username: userSplit[0], |  | ||||||
| 			Password: userSplit[1], |  | ||||||
| 		}) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Parsed users") | 	log.Debug().Msg("Parsed users") | ||||||
| @@ -195,6 +191,8 @@ func GetTinyauthLabels(labels map[string]string) types.TinyauthLabels { | |||||||
| 				tinyauthLabels.OAuthWhitelist = strings.Split(value, ",") | 				tinyauthLabels.OAuthWhitelist = strings.Split(value, ",") | ||||||
| 			case "tinyauth.users": | 			case "tinyauth.users": | ||||||
| 				tinyauthLabels.Users = strings.Split(value, ",") | 				tinyauthLabels.Users = strings.Split(value, ",") | ||||||
|  | 			case "tinyauth.allowed": | ||||||
|  | 				tinyauthLabels.Allowed = value | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @@ -217,3 +215,43 @@ func Filter[T any](slice []T, test func(T) bool) (res []T) { | |||||||
| 	} | 	} | ||||||
| 	return res | 	return res | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Parse user | ||||||
|  | func ParseUser(user string) (types.User, error) { | ||||||
|  | 	// Check if the user is escaped | ||||||
|  | 	if strings.Contains(user, "$$") { | ||||||
|  | 		user = strings.ReplaceAll(user, "$$", "$") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Split the user by colon | ||||||
|  | 	userSplit := strings.Split(user, ":") | ||||||
|  |  | ||||||
|  | 	// Check if the user is in the correct format | ||||||
|  | 	if len(userSplit) < 2 || len(userSplit) > 3 { | ||||||
|  | 		return types.User{}, errors.New("invalid user format") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Check if the user has a totp secret | ||||||
|  | 	if len(userSplit) == 2 { | ||||||
|  | 		// Check for empty username or password | ||||||
|  | 		if userSplit[1] == "" || userSplit[0] == "" { | ||||||
|  | 			return types.User{}, errors.New("invalid user format") | ||||||
|  | 		} | ||||||
|  | 		return types.User{ | ||||||
|  | 			Username: userSplit[0], | ||||||
|  | 			Password: userSplit[1], | ||||||
|  | 		}, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Check for empty username, password or totp secret | ||||||
|  | 	if userSplit[2] == "" || userSplit[1] == "" || userSplit[0] == "" { | ||||||
|  | 		return types.User{}, errors.New("invalid user format") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Return the user struct | ||||||
|  | 	return types.User{ | ||||||
|  | 		Username:   userSplit[0], | ||||||
|  | 		Password:   userSplit[1], | ||||||
|  | 		TotpSecret: userSplit[2], | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|   | |||||||
| @@ -36,18 +36,6 @@ func TestParseUsers(t *testing.T) { | |||||||
| 	if !reflect.DeepEqual(expected, result) { | 	if !reflect.DeepEqual(expected, result) { | ||||||
| 		t.Fatalf("Expected %v, got %v", expected, result) | 		t.Fatalf("Expected %v, got %v", expected, result) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	t.Log("Testing parse users with an invalid string") |  | ||||||
|  |  | ||||||
| 	// Test the parse users function with an invalid string |  | ||||||
| 	users = "user1:pass1,user2" |  | ||||||
|  |  | ||||||
| 	_, err = utils.ParseUsers(users) |  | ||||||
|  |  | ||||||
| 	// There should be an error |  | ||||||
| 	if err == nil { |  | ||||||
| 		t.Fatalf("Expected error parsing users") |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // Test the get root url function | // Test the get root url function | ||||||
| @@ -298,12 +286,14 @@ func TestGetTinyauthLabels(t *testing.T) { | |||||||
| 	labels := map[string]string{ | 	labels := map[string]string{ | ||||||
| 		"tinyauth.users":           "user1,user2", | 		"tinyauth.users":           "user1,user2", | ||||||
| 		"tinyauth.oauth.whitelist": "user1,user2", | 		"tinyauth.oauth.whitelist": "user1,user2", | ||||||
|  | 		"tinyauth.allowed":         "random", | ||||||
| 		"random":                   "random", | 		"random":                   "random", | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	expected := types.TinyauthLabels{ | 	expected := types.TinyauthLabels{ | ||||||
| 		Users:          []string{"user1", "user2"}, | 		Users:          []string{"user1", "user2"}, | ||||||
| 		OAuthWhitelist: []string{"user1", "user2"}, | 		OAuthWhitelist: []string{"user1", "user2"}, | ||||||
|  | 		Allowed:        "random", | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	result := utils.GetTinyauthLabels(labels) | 	result := utils.GetTinyauthLabels(labels) | ||||||
| @@ -332,3 +322,65 @@ func TestFilter(t *testing.T) { | |||||||
| 		t.Fatalf("Expected %v, got %v", expected, result) | 		t.Fatalf("Expected %v, got %v", expected, result) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Test parse user | ||||||
|  | func TestParseUser(t *testing.T) { | ||||||
|  | 	t.Log("Testing parse user with a valid user") | ||||||
|  |  | ||||||
|  | 	// Create variables | ||||||
|  | 	user := "user:pass:secret" | ||||||
|  | 	expected := types.User{ | ||||||
|  | 		Username:   "user", | ||||||
|  | 		Password:   "pass", | ||||||
|  | 		TotpSecret: "secret", | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Test the parse user function | ||||||
|  | 	result, err := utils.ParseUser(user) | ||||||
|  |  | ||||||
|  | 	// Check if there was an error | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Error parsing user: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Check if the result is equal to the expected | ||||||
|  | 	if !reflect.DeepEqual(expected, result) { | ||||||
|  | 		t.Fatalf("Expected %v, got %v", expected, result) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	t.Log("Testing parse user with an escaped user") | ||||||
|  |  | ||||||
|  | 	// Create variables | ||||||
|  | 	user = "user:p$$ass$$:secret" | ||||||
|  | 	expected = types.User{ | ||||||
|  | 		Username:   "user", | ||||||
|  | 		Password:   "p$ass$", | ||||||
|  | 		TotpSecret: "secret", | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Test the parse user function | ||||||
|  | 	result, err = utils.ParseUser(user) | ||||||
|  |  | ||||||
|  | 	// Check if there was an error | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Error parsing user: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Check if the result is equal to the expected | ||||||
|  | 	if !reflect.DeepEqual(expected, result) { | ||||||
|  | 		t.Fatalf("Expected %v, got %v", expected, result) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	t.Log("Testing parse user with an invalid user") | ||||||
|  |  | ||||||
|  | 	// Create variables | ||||||
|  | 	user = "user::pass" | ||||||
|  |  | ||||||
|  | 	// Test the parse user function | ||||||
|  | 	_, err = utils.ParseUser(user) | ||||||
|  |  | ||||||
|  | 	// Check if there was an error | ||||||
|  | 	if err == nil { | ||||||
|  | 		t.Fatalf("Expected error parsing user") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										46
									
								
								site/src/components/auth/login-forn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								site/src/components/auth/login-forn.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | |||||||
|  | import { TextInput, PasswordInput, Button } from "@mantine/core"; | ||||||
|  | import { useForm, zodResolver } from "@mantine/form"; | ||||||
|  | import { LoginFormValues, loginSchema } from "../../schemas/login-schema"; | ||||||
|  |  | ||||||
|  | interface LoginFormProps { | ||||||
|  |   isLoading: boolean; | ||||||
|  |   onSubmit: (values: LoginFormValues) => void; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const LoginForm = (props: LoginFormProps) => { | ||||||
|  |   const { isLoading, onSubmit } = props; | ||||||
|  |  | ||||||
|  |   const form = useForm({ | ||||||
|  |     mode: "uncontrolled", | ||||||
|  |     initialValues: { | ||||||
|  |       username: "", | ||||||
|  |       password: "", | ||||||
|  |     }, | ||||||
|  |     validate: zodResolver(loginSchema), | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <form onSubmit={form.onSubmit(onSubmit)}> | ||||||
|  |       <TextInput | ||||||
|  |         label="Username" | ||||||
|  |         placeholder="user@example.com" | ||||||
|  |         required | ||||||
|  |         disabled={isLoading} | ||||||
|  |         key={form.key("username")} | ||||||
|  |         {...form.getInputProps("username")} | ||||||
|  |       /> | ||||||
|  |       <PasswordInput | ||||||
|  |         label="Password" | ||||||
|  |         placeholder="password" | ||||||
|  |         required | ||||||
|  |         mt="md" | ||||||
|  |         disabled={isLoading} | ||||||
|  |         key={form.key("password")} | ||||||
|  |         {...form.getInputProps("password")} | ||||||
|  |       /> | ||||||
|  |       <Button fullWidth mt="xl" type="submit" loading={isLoading}> | ||||||
|  |         Login | ||||||
|  |       </Button> | ||||||
|  |     </form> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
							
								
								
									
										72
									
								
								site/src/components/auth/oauth-buttons.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								site/src/components/auth/oauth-buttons.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | |||||||
|  | import { Grid, Button } from "@mantine/core"; | ||||||
|  | import { GithubIcon } from "../../icons/github"; | ||||||
|  | import { GoogleIcon } from "../../icons/google"; | ||||||
|  | import { OAuthIcon } from "../../icons/oauth"; | ||||||
|  | import { TailscaleIcon } from "../../icons/tailscale"; | ||||||
|  |  | ||||||
|  | interface OAuthButtonsProps { | ||||||
|  |   oauthProviders: string[]; | ||||||
|  |   isLoading: boolean; | ||||||
|  |   mutate: (provider: string) => void; | ||||||
|  |   genericName: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const OAuthButtons = (props: OAuthButtonsProps) => { | ||||||
|  |   const { oauthProviders, isLoading, genericName, mutate } = props; | ||||||
|  |   return ( | ||||||
|  |     <Grid mb="md" mt="md" align="center" justify="center"> | ||||||
|  |       {oauthProviders.includes("google") && ( | ||||||
|  |         <Grid.Col span="content"> | ||||||
|  |           <Button | ||||||
|  |             radius="xl" | ||||||
|  |             leftSection={<GoogleIcon style={{ width: 14, height: 14 }} />} | ||||||
|  |             variant="default" | ||||||
|  |             onClick={() => mutate("google")} | ||||||
|  |             loading={isLoading} | ||||||
|  |           > | ||||||
|  |             Google | ||||||
|  |           </Button> | ||||||
|  |         </Grid.Col> | ||||||
|  |       )} | ||||||
|  |       {oauthProviders.includes("github") && ( | ||||||
|  |         <Grid.Col span="content"> | ||||||
|  |           <Button | ||||||
|  |             radius="xl" | ||||||
|  |             leftSection={<GithubIcon style={{ width: 14, height: 14 }} />} | ||||||
|  |             variant="default" | ||||||
|  |             onClick={() => mutate("github")} | ||||||
|  |             loading={isLoading} | ||||||
|  |           > | ||||||
|  |             Github | ||||||
|  |           </Button> | ||||||
|  |         </Grid.Col> | ||||||
|  |       )} | ||||||
|  |       {oauthProviders.includes("tailscale") && ( | ||||||
|  |         <Grid.Col span="content"> | ||||||
|  |           <Button | ||||||
|  |             radius="xl" | ||||||
|  |             leftSection={<TailscaleIcon style={{ width: 14, height: 14 }} />} | ||||||
|  |             variant="default" | ||||||
|  |             onClick={() => mutate("tailscale")} | ||||||
|  |             loading={isLoading} | ||||||
|  |           > | ||||||
|  |             Tailscale | ||||||
|  |           </Button> | ||||||
|  |         </Grid.Col> | ||||||
|  |       )} | ||||||
|  |       {oauthProviders.includes("generic") && ( | ||||||
|  |         <Grid.Col span="content"> | ||||||
|  |           <Button | ||||||
|  |             radius="xl" | ||||||
|  |             leftSection={<OAuthIcon style={{ width: 14, height: 14 }} />} | ||||||
|  |             variant="default" | ||||||
|  |             onClick={() => mutate("generic")} | ||||||
|  |             loading={isLoading} | ||||||
|  |           > | ||||||
|  |             {genericName} | ||||||
|  |           </Button> | ||||||
|  |         </Grid.Col> | ||||||
|  |       )} | ||||||
|  |     </Grid> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
							
								
								
									
										40
									
								
								site/src/components/auth/totp-form.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								site/src/components/auth/totp-form.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | import { Button, PinInput } from "@mantine/core"; | ||||||
|  | import { useForm, zodResolver } from "@mantine/form"; | ||||||
|  | import { z } from "zod"; | ||||||
|  |  | ||||||
|  | const schema = z.object({ | ||||||
|  |   code: z.string(), | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | type FormValues = z.infer<typeof schema>; | ||||||
|  |  | ||||||
|  | interface TotpFormProps { | ||||||
|  |   onSubmit: (values: FormValues) => void; | ||||||
|  |   isLoading: boolean; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const TotpForm = (props: TotpFormProps) => { | ||||||
|  |   const { onSubmit, isLoading } = props; | ||||||
|  |  | ||||||
|  |   const form = useForm({ | ||||||
|  |     mode: "uncontrolled", | ||||||
|  |     initialValues: { | ||||||
|  |       code: "", | ||||||
|  |     }, | ||||||
|  |     validate: zodResolver(schema), | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <form onSubmit={form.onSubmit(onSubmit)}> | ||||||
|  |       <PinInput | ||||||
|  |         length={6} | ||||||
|  |         type={"number"} | ||||||
|  |         placeholder="" | ||||||
|  |         {...form.getInputProps("code")} | ||||||
|  |       /> | ||||||
|  |       <Button type="submit" mt="xl" loading={isLoading} fullWidth> | ||||||
|  |         Verify | ||||||
|  |       </Button> | ||||||
|  |     </form> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { useQuery } from "@tanstack/react-query"; | import { useQuery } from "@tanstack/react-query"; | ||||||
| import React, { createContext, useContext } from "react"; | import React, { createContext, useContext } from "react"; | ||||||
| import { UserContextSchemaType } from "../schemas/user-context-schema"; |  | ||||||
| import axios from "axios"; | import axios from "axios"; | ||||||
|  | import { UserContextSchemaType } from "../schemas/user-context-schema"; | ||||||
|  |  | ||||||
| const UserContext = createContext<UserContextSchemaType | null>(null); | const UserContext = createContext<UserContextSchemaType | null>(null); | ||||||
|  |  | ||||||
| @@ -15,7 +15,7 @@ export const UserContextProvider = ({ | |||||||
|     isLoading, |     isLoading, | ||||||
|     error, |     error, | ||||||
|   } = useQuery({ |   } = useQuery({ | ||||||
|     queryKey: ["isLoggedIn"], |     queryKey: ["userContext"], | ||||||
|     queryFn: async () => { |     queryFn: async () => { | ||||||
|       const res = await axios.get("/api/status"); |       const res = await axios.get("/api/status"); | ||||||
|       return res.data; |       return res.data; | ||||||
|   | |||||||
| @@ -15,6 +15,7 @@ import { ContinuePage } from "./pages/continue-page.tsx"; | |||||||
| import { NotFoundPage } from "./pages/not-found-page.tsx"; | import { NotFoundPage } from "./pages/not-found-page.tsx"; | ||||||
| import { UnauthorizedPage } from "./pages/unauthorized-page.tsx"; | import { UnauthorizedPage } from "./pages/unauthorized-page.tsx"; | ||||||
| import { InternalServerError } from "./pages/internal-server-error.tsx"; | import { InternalServerError } from "./pages/internal-server-error.tsx"; | ||||||
|  | import { TotpPage } from "./pages/totp-page.tsx"; | ||||||
|  |  | ||||||
| const queryClient = new QueryClient({ | const queryClient = new QueryClient({ | ||||||
|   defaultOptions: { |   defaultOptions: { | ||||||
| @@ -34,6 +35,7 @@ createRoot(document.getElementById("root")!).render( | |||||||
|             <Routes> |             <Routes> | ||||||
|               <Route path="/" element={<App />} /> |               <Route path="/" element={<App />} /> | ||||||
|               <Route path="/login" element={<LoginPage />} /> |               <Route path="/login" element={<LoginPage />} /> | ||||||
|  |               <Route path="/totp" element={<TotpPage />} /> | ||||||
|               <Route path="/logout" element={<LogoutPage />} /> |               <Route path="/logout" element={<LogoutPage />} /> | ||||||
|               <Route path="/continue" element={<ContinuePage />} /> |               <Route path="/continue" element={<ContinuePage />} /> | ||||||
|               <Route path="/unauthorized" element={<UnauthorizedPage />} /> |               <Route path="/unauthorized" element={<UnauthorizedPage />} /> | ||||||
|   | |||||||
| @@ -50,26 +50,6 @@ export const ContinuePage = () => { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if ( |  | ||||||
|     window.location.protocol === "https:" && |  | ||||||
|     uri.protocol === "http:" |  | ||||||
|   ) { |  | ||||||
|     return ( |  | ||||||
|       <ContinuePageLayout> |  | ||||||
|         <Text size="xl" fw={700}> |  | ||||||
|           Insecure Redirect |  | ||||||
|         </Text> |  | ||||||
|         <Text> |  | ||||||
|           Your are logged in but trying to redirect from <Code>https</Code> to{" "} |  | ||||||
|           <Code>http</Code>, please click the button to redirect. |  | ||||||
|         </Text> |  | ||||||
|         <Button fullWidth mt="xl" onClick={redirect}> |  | ||||||
|           Continue |  | ||||||
|         </Button> |  | ||||||
|       </ContinuePageLayout> |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if (disableContinue) { |   if (disableContinue) { | ||||||
|     window.location.href = redirectUri; |     window.location.href = redirectUri; | ||||||
|     return ( |     return ( | ||||||
| @@ -82,6 +62,23 @@ export const ContinuePage = () => { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   if (window.location.protocol === "https:" && uri.protocol === "http:") { | ||||||
|  |     return ( | ||||||
|  |       <ContinuePageLayout> | ||||||
|  |         <Text size="xl" fw={700}> | ||||||
|  |           Insecure Redirect | ||||||
|  |         </Text> | ||||||
|  |         <Text> | ||||||
|  |           Your are trying to redirect from <Code>https</Code> to{" "} | ||||||
|  |           <Code>http</Code>, are you sure you want to continue? | ||||||
|  |         </Text> | ||||||
|  |         <Button fullWidth mt="xl" color="yellow" onClick={redirect}> | ||||||
|  |           Continue | ||||||
|  |         </Button> | ||||||
|  |       </ContinuePageLayout> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <ContinuePageLayout> |     <ContinuePageLayout> | ||||||
|       <Text size="xl" fw={700}> |       <Text size="xl" fw={700}> | ||||||
|   | |||||||
| @@ -1,25 +1,13 @@ | |||||||
| import { | import { Paper, Title, Text, Divider } from "@mantine/core"; | ||||||
|   Button, |  | ||||||
|   Paper, |  | ||||||
|   PasswordInput, |  | ||||||
|   TextInput, |  | ||||||
|   Title, |  | ||||||
|   Text, |  | ||||||
|   Divider, |  | ||||||
|   Grid, |  | ||||||
| } from "@mantine/core"; |  | ||||||
| import { useForm, zodResolver } from "@mantine/form"; |  | ||||||
| import { notifications } from "@mantine/notifications"; | import { notifications } from "@mantine/notifications"; | ||||||
| import { useMutation } from "@tanstack/react-query"; | import { useMutation } from "@tanstack/react-query"; | ||||||
| import axios from "axios"; | import axios from "axios"; | ||||||
| import { z } from "zod"; |  | ||||||
| import { useUserContext } from "../context/user-context"; | import { useUserContext } from "../context/user-context"; | ||||||
| import { Navigate } from "react-router"; | import { Navigate } from "react-router"; | ||||||
| import { Layout } from "../components/layouts/layout"; | import { Layout } from "../components/layouts/layout"; | ||||||
| import { GoogleIcon } from "../icons/google"; | import { OAuthButtons } from "../components/auth/oauth-buttons"; | ||||||
| import { GithubIcon } from "../icons/github"; | import { LoginFormValues } from "../schemas/login-schema"; | ||||||
| import { OAuthIcon } from "../icons/oauth"; | import { LoginForm } from "../components/auth/login-forn"; | ||||||
| import { TailscaleIcon } from "../icons/tailscale"; |  | ||||||
| import { isQueryValid } from "../utils/utils"; | import { isQueryValid } from "../utils/utils"; | ||||||
|  |  | ||||||
| export const LoginPage = () => { | export const LoginPage = () => { | ||||||
| @@ -27,7 +15,8 @@ export const LoginPage = () => { | |||||||
|   const params = new URLSearchParams(queryString); |   const params = new URLSearchParams(queryString); | ||||||
|   const redirectUri = params.get("redirect_uri") ?? ""; |   const redirectUri = params.get("redirect_uri") ?? ""; | ||||||
|  |  | ||||||
|   const { isLoggedIn, configuredProviders } = useUserContext(); |   const { isLoggedIn, configuredProviders, title, genericName } = | ||||||
|  |     useUserContext(); | ||||||
|  |  | ||||||
|   const oauthProviders = configuredProviders.filter( |   const oauthProviders = configuredProviders.filter( | ||||||
|     (value) => value !== "username", |     (value) => value !== "username", | ||||||
| @@ -37,24 +26,8 @@ export const LoginPage = () => { | |||||||
|     return <Navigate to="/logout" />; |     return <Navigate to="/logout" />; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const schema = z.object({ |  | ||||||
|     username: z.string(), |  | ||||||
|     password: z.string(), |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   type FormValues = z.infer<typeof schema>; |  | ||||||
|  |  | ||||||
|   const form = useForm({ |  | ||||||
|     mode: "uncontrolled", |  | ||||||
|     initialValues: { |  | ||||||
|       username: "", |  | ||||||
|       password: "", |  | ||||||
|     }, |  | ||||||
|     validate: zodResolver(schema), |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   const loginMutation = useMutation({ |   const loginMutation = useMutation({ | ||||||
|     mutationFn: (login: FormValues) => { |     mutationFn: (login: LoginFormValues) => { | ||||||
|       return axios.post("/api/login", login); |       return axios.post("/api/login", login); | ||||||
|     }, |     }, | ||||||
|     onError: () => { |     onError: () => { | ||||||
| @@ -64,18 +37,25 @@ export const LoginPage = () => { | |||||||
|         color: "red", |         color: "red", | ||||||
|       }); |       }); | ||||||
|     }, |     }, | ||||||
|     onSuccess: () => { |     onSuccess: async (data) => { | ||||||
|  |       if (data.data.totpPending) { | ||||||
|  |         window.location.replace(`/totp?redirect_uri=${redirectUri}`); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|       notifications.show({ |       notifications.show({ | ||||||
|         title: "Logged in", |         title: "Logged in", | ||||||
|         message: "Welcome back!", |         message: "Welcome back!", | ||||||
|         color: "green", |         color: "green", | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       setTimeout(() => { |       setTimeout(() => { | ||||||
|         if (isQueryValid(redirectUri)) { |         if (!isQueryValid(redirectUri)) { | ||||||
|           window.location.replace("/"); |           window.location.replace("/"); | ||||||
|         } else { |           return; | ||||||
|           window.location.replace(`/continue?redirect_uri=${redirectUri}`); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         window.location.replace(`/continue?redirect_uri=${redirectUri}`); | ||||||
|       }, 500); |       }, 500); | ||||||
|     }, |     }, | ||||||
|   }); |   }); | ||||||
| @@ -105,81 +85,25 @@ export const LoginPage = () => { | |||||||
|     }, |     }, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   const handleSubmit = (values: FormValues) => { |   const handleSubmit = (values: LoginFormValues) => { | ||||||
|     loginMutation.mutate(values); |     loginMutation.mutate(values); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Layout> |     <Layout> | ||||||
|       <Title ta="center">Tinyauth</Title> |       <Title ta="center">{title}</Title> | ||||||
|       <Paper shadow="md" p="xl" mt={30} radius="md" withBorder> |       <Paper shadow="md" p="xl" mt={30} radius="md" withBorder> | ||||||
|         {oauthProviders.length > 0 && ( |         {oauthProviders.length > 0 && ( | ||||||
|           <> |           <> | ||||||
|             <Text size="lg" fw={500} ta="center"> |             <Text size="lg" fw={500} ta="center"> | ||||||
|               Welcome back, login with |               Welcome back, login with | ||||||
|             </Text> |             </Text> | ||||||
|             <Grid mb="md" mt="md" align="center" justify="center"> |             <OAuthButtons | ||||||
|               {oauthProviders.includes("google") && ( |               oauthProviders={oauthProviders} | ||||||
|                 <Grid.Col span="content"> |               isLoading={loginOAuthMutation.isLoading} | ||||||
|                   <Button |               mutate={loginOAuthMutation.mutate} | ||||||
|                     radius="xl" |               genericName={genericName} | ||||||
|                     leftSection={ |             /> | ||||||
|                       <GoogleIcon style={{ width: 14, height: 14 }} /> |  | ||||||
|                     } |  | ||||||
|                     variant="default" |  | ||||||
|                     onClick={() => loginOAuthMutation.mutate("google")} |  | ||||||
|                     loading={loginOAuthMutation.isLoading} |  | ||||||
|                   > |  | ||||||
|                     Google |  | ||||||
|                   </Button> |  | ||||||
|                 </Grid.Col> |  | ||||||
|               )} |  | ||||||
|               {oauthProviders.includes("github") && ( |  | ||||||
|                 <Grid.Col span="content"> |  | ||||||
|                   <Button |  | ||||||
|                     radius="xl" |  | ||||||
|                     leftSection={ |  | ||||||
|                       <GithubIcon style={{ width: 14, height: 14 }} /> |  | ||||||
|                     } |  | ||||||
|                     variant="default" |  | ||||||
|                     onClick={() => loginOAuthMutation.mutate("github")} |  | ||||||
|                     loading={loginOAuthMutation.isLoading} |  | ||||||
|                   > |  | ||||||
|                     Github |  | ||||||
|                   </Button> |  | ||||||
|                 </Grid.Col> |  | ||||||
|               )} |  | ||||||
|               {oauthProviders.includes("tailscale") && ( |  | ||||||
|                 <Grid.Col span="content"> |  | ||||||
|                   <Button |  | ||||||
|                     radius="xl" |  | ||||||
|                     leftSection={ |  | ||||||
|                       <TailscaleIcon style={{ width: 14, height: 14 }} /> |  | ||||||
|                     } |  | ||||||
|                     variant="default" |  | ||||||
|                     onClick={() => loginOAuthMutation.mutate("tailscale")} |  | ||||||
|                     loading={loginOAuthMutation.isLoading} |  | ||||||
|                   > |  | ||||||
|                     Tailscale |  | ||||||
|                   </Button> |  | ||||||
|                 </Grid.Col> |  | ||||||
|               )} |  | ||||||
|               {oauthProviders.includes("generic") && ( |  | ||||||
|                 <Grid.Col span="content"> |  | ||||||
|                   <Button |  | ||||||
|                     radius="xl" |  | ||||||
|                     leftSection={ |  | ||||||
|                       <OAuthIcon style={{ width: 14, height: 14 }} /> |  | ||||||
|                     } |  | ||||||
|                     variant="default" |  | ||||||
|                     onClick={() => loginOAuthMutation.mutate("generic")} |  | ||||||
|                     loading={loginOAuthMutation.isLoading} |  | ||||||
|                   > |  | ||||||
|                     Generic |  | ||||||
|                   </Button> |  | ||||||
|                 </Grid.Col> |  | ||||||
|               )} |  | ||||||
|             </Grid> |  | ||||||
|             {configuredProviders.includes("username") && ( |             {configuredProviders.includes("username") && ( | ||||||
|               <Divider |               <Divider | ||||||
|                 label="Or continue with password" |                 label="Or continue with password" | ||||||
| @@ -190,33 +114,10 @@ export const LoginPage = () => { | |||||||
|           </> |           </> | ||||||
|         )} |         )} | ||||||
|         {configuredProviders.includes("username") && ( |         {configuredProviders.includes("username") && ( | ||||||
|           <form onSubmit={form.onSubmit(handleSubmit)}> |           <LoginForm | ||||||
|             <TextInput |             isLoading={loginMutation.isLoading} | ||||||
|               label="Username" |             onSubmit={handleSubmit} | ||||||
|               placeholder="user@example.com" |  | ||||||
|               required |  | ||||||
|               disabled={loginMutation.isLoading} |  | ||||||
|               key={form.key("username")} |  | ||||||
|               {...form.getInputProps("username")} |  | ||||||
|           /> |           /> | ||||||
|             <PasswordInput |  | ||||||
|               label="Password" |  | ||||||
|               placeholder="password" |  | ||||||
|               required |  | ||||||
|               mt="md" |  | ||||||
|               disabled={loginMutation.isLoading} |  | ||||||
|               key={form.key("password")} |  | ||||||
|               {...form.getInputProps("password")} |  | ||||||
|             /> |  | ||||||
|             <Button |  | ||||||
|               fullWidth |  | ||||||
|               mt="xl" |  | ||||||
|               type="submit" |  | ||||||
|               loading={loginMutation.isLoading} |  | ||||||
|             > |  | ||||||
|               Login |  | ||||||
|             </Button> |  | ||||||
|           </form> |  | ||||||
|         )} |         )} | ||||||
|       </Paper> |       </Paper> | ||||||
|     </Layout> |     </Layout> | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ import { Layout } from "../components/layouts/layout"; | |||||||
| import { capitalize } from "../utils/utils"; | import { capitalize } from "../utils/utils"; | ||||||
|  |  | ||||||
| export const LogoutPage = () => { | export const LogoutPage = () => { | ||||||
|   const { isLoggedIn, username, oauth, provider } = useUserContext(); |   const { isLoggedIn, username, oauth, provider, genericName } = useUserContext(); | ||||||
|  |  | ||||||
|   if (!isLoggedIn) { |   if (!isLoggedIn) { | ||||||
|     return <Navigate to="/login" />; |     return <Navigate to="/login" />; | ||||||
| @@ -45,7 +45,7 @@ export const LogoutPage = () => { | |||||||
|         </Text> |         </Text> | ||||||
|         <Text> |         <Text> | ||||||
|           You are currently logged in as <Code>{username}</Code> |           You are currently logged in as <Code>{username}</Code> | ||||||
|           {oauth && ` using ${capitalize(provider)} OAuth`}. Click the button |           {oauth && ` using ${capitalize(provider === "generic" ? genericName : provider)} OAuth`}. Click the button | ||||||
|           below to log out. |           below to log out. | ||||||
|         </Text> |         </Text> | ||||||
|         <Button |         <Button | ||||||
|   | |||||||
							
								
								
									
										62
									
								
								site/src/pages/totp-page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								site/src/pages/totp-page.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | |||||||
|  | import { Navigate } from "react-router"; | ||||||
|  | import { useUserContext } from "../context/user-context"; | ||||||
|  | import { Title, Paper, Text } from "@mantine/core"; | ||||||
|  | import { Layout } from "../components/layouts/layout"; | ||||||
|  | import { TotpForm } from "../components/auth/totp-form"; | ||||||
|  | import { useMutation } from "@tanstack/react-query"; | ||||||
|  | import axios from "axios"; | ||||||
|  | import { notifications } from "@mantine/notifications"; | ||||||
|  |  | ||||||
|  | export const TotpPage = () => { | ||||||
|  |   const queryString = window.location.search; | ||||||
|  |   const params = new URLSearchParams(queryString); | ||||||
|  |   const redirectUri = params.get("redirect_uri") ?? ""; | ||||||
|  |  | ||||||
|  |   const { totpPending, isLoggedIn, title } = useUserContext(); | ||||||
|  |  | ||||||
|  |   if (isLoggedIn) { | ||||||
|  |     return <Navigate to={`/logout`} />; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (!totpPending) { | ||||||
|  |     return <Navigate to={`/login?redirect_uri=${redirectUri}`} />; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const totpMutation = useMutation({ | ||||||
|  |     mutationFn: async (totp: { code: string }) => { | ||||||
|  |       await axios.post("/api/totp", totp); | ||||||
|  |     }, | ||||||
|  |     onError: () => { | ||||||
|  |       notifications.show({ | ||||||
|  |         title: "Failed to verify code", | ||||||
|  |         message: "Please try again", | ||||||
|  |         color: "red", | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |     onSuccess: () => { | ||||||
|  |       notifications.show({ | ||||||
|  |         title: "Verified", | ||||||
|  |         message: "Redirecting to your app", | ||||||
|  |         color: "green", | ||||||
|  |       }); | ||||||
|  |       setTimeout(() => { | ||||||
|  |         window.location.replace(`/continue?redirect_uri=${redirectUri}`); | ||||||
|  |       }, 500); | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Layout> | ||||||
|  |       <Title ta="center">{title}</Title> | ||||||
|  |       <Paper shadow="md" p="xl" mt={30} radius="md" withBorder> | ||||||
|  |         <Text size="lg" fw={500} mb="md" ta="center"> | ||||||
|  |           Enter your TOTP code | ||||||
|  |         </Text> | ||||||
|  |         <TotpForm | ||||||
|  |           isLoading={totpMutation.isLoading} | ||||||
|  |           onSubmit={(values) => totpMutation.mutate(values)} | ||||||
|  |         /> | ||||||
|  |       </Paper> | ||||||
|  |     </Layout> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
							
								
								
									
										8
									
								
								site/src/schemas/login-schema.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								site/src/schemas/login-schema.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | import { z } from "zod"; | ||||||
|  |  | ||||||
|  | export const loginSchema = z.object({ | ||||||
|  |   username: z.string(), | ||||||
|  |   password: z.string(), | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export type LoginFormValues = z.infer<typeof loginSchema>; | ||||||
| @@ -7,6 +7,9 @@ export const userContextSchema = z.object({ | |||||||
|   provider: z.string(), |   provider: z.string(), | ||||||
|   configuredProviders: z.array(z.string()), |   configuredProviders: z.array(z.string()), | ||||||
|   disableContinue: z.boolean(), |   disableContinue: z.boolean(), | ||||||
|  |   title: z.string(), | ||||||
|  |   genericName: z.string(), | ||||||
|  |   totpPending: z.boolean(), | ||||||
| }); | }); | ||||||
|  |  | ||||||
| export type UserContextSchemaType = z.infer<typeof userContextSchema>; | export type UserContextSchemaType = z.infer<typeof userContextSchema>; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user