mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-11-03 23:55:44 +00:00 
			
		
		
		
	Compare commits
	
		
			16 Commits
		
	
	
		
			v2.0.0-alp
			...
			v2.1.0-alp
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					c37f66abb9 | ||
| 
						 | 
					2c4f086008 | ||
| 
						 | 
					6e5f882e0b | ||
| 
						 | 
					99268f80c9 | ||
| 
						 | 
					dcd816b6c6 | ||
| 
						 | 
					381f6ef76f | ||
| 
						 | 
					8a8ba18ded | ||
| 
						 | 
					29f0a94faf | ||
| 
						 | 
					6602e8140b | ||
| 
						 | 
					2385599c80 | ||
| 
						 | 
					6f184856f1 | ||
| 
						 | 
					e2e3b3bdc6 | ||
| 
						 | 
					3efcb26db1 | ||
| 
						 | 
					c54267f50d | ||
| 
						 | 
					4de12ce5c1 | ||
| 
						 | 
					0cf0aafc14 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -12,3 +12,4 @@ users.txt
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# secret test file
 | 
					# secret test file
 | 
				
			||||||
secret.txt
 | 
					secret.txt
 | 
				
			||||||
 | 
					secret_oauth.txt
 | 
				
			||||||
@@ -35,7 +35,7 @@ COPY ./cmd ./cmd
 | 
				
			|||||||
COPY ./internal ./internal
 | 
					COPY ./internal ./internal
 | 
				
			||||||
COPY --from=site-builder /site/dist ./internal/assets/dist
 | 
					COPY --from=site-builder /site/dist ./internal/assets/dist
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RUN CGO_ENABLED=0 go build
 | 
					RUN CGO_ENABLED=0 go build -ldflags "-s -w"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Runner
 | 
					# Runner
 | 
				
			||||||
FROM alpine:3.21 AS runner
 | 
					FROM alpine:3.21 AS runner
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										76
									
								
								cmd/root.go
									
									
									
									
									
								
							
							
						
						
									
										76
									
								
								cmd/root.go
									
									
									
									
									
								
							@@ -1,10 +1,14 @@
 | 
				
			|||||||
package cmd
 | 
					package cmd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
	cmd "tinyauth/cmd/user"
 | 
						cmd "tinyauth/cmd/user"
 | 
				
			||||||
	"tinyauth/internal/api"
 | 
						"tinyauth/internal/api"
 | 
				
			||||||
 | 
						"tinyauth/internal/assets"
 | 
				
			||||||
	"tinyauth/internal/auth"
 | 
						"tinyauth/internal/auth"
 | 
				
			||||||
 | 
						"tinyauth/internal/docker"
 | 
				
			||||||
	"tinyauth/internal/hooks"
 | 
						"tinyauth/internal/hooks"
 | 
				
			||||||
	"tinyauth/internal/providers"
 | 
						"tinyauth/internal/providers"
 | 
				
			||||||
	"tinyauth/internal/types"
 | 
						"tinyauth/internal/types"
 | 
				
			||||||
@@ -22,21 +26,29 @@ var rootCmd = &cobra.Command{
 | 
				
			|||||||
	Short: "The simplest way to protect your apps with a login screen.",
 | 
						Short: "The simplest way to protect your apps with a login screen.",
 | 
				
			||||||
	Long:  `Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic OAuth provider to all of your docker apps.`,
 | 
						Long:  `Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic OAuth provider to all of your docker apps.`,
 | 
				
			||||||
	Run: func(cmd *cobra.Command, args []string) {
 | 
						Run: func(cmd *cobra.Command, args []string) {
 | 
				
			||||||
 | 
							// Logger
 | 
				
			||||||
 | 
							log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Logger().Level(zerolog.FatalLevel)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Get config
 | 
							// Get config
 | 
				
			||||||
		log.Info().Msg("Parsing config")
 | 
					 | 
				
			||||||
		var config types.Config
 | 
							var config types.Config
 | 
				
			||||||
		parseErr := viper.Unmarshal(&config)
 | 
							parseErr := viper.Unmarshal(&config)
 | 
				
			||||||
		HandleError(parseErr, "Failed to parse config")
 | 
							HandleError(parseErr, "Failed to parse config")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Secrets
 | 
				
			||||||
 | 
							config.Secret = utils.GetSecret(config.Secret, config.SecretFile)
 | 
				
			||||||
 | 
							config.GithubClientSecret = utils.GetSecret(config.GithubClientSecret, config.GithubClientSecretFile)
 | 
				
			||||||
 | 
							config.GoogleClientSecret = utils.GetSecret(config.GoogleClientSecret, config.GoogleClientSecretFile)
 | 
				
			||||||
 | 
							config.GenericClientSecret = utils.GetSecret(config.GenericClientSecret, config.GenericClientSecretFile)
 | 
				
			||||||
 | 
							config.TailscaleClientSecret = utils.GetSecret(config.TailscaleClientSecret, config.TailscaleClientSecretFile)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Validate config
 | 
							// Validate config
 | 
				
			||||||
		log.Info().Msg("Validating config")
 | 
					 | 
				
			||||||
		validator := validator.New()
 | 
							validator := validator.New()
 | 
				
			||||||
		validateErr := validator.Struct(config)
 | 
							validateErr := validator.Struct(config)
 | 
				
			||||||
		HandleError(validateErr, "Invalid config")
 | 
							HandleError(validateErr, "Failed to validate config")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Set log level
 | 
							// Logger
 | 
				
			||||||
		log.Info().Int8("log_level", config.LogLevel).Msg("Setting log level")
 | 
							log.Logger = log.Level(zerolog.Level(config.LogLevel))
 | 
				
			||||||
		log.Logger = log.Logger.Level(zerolog.Level(config.LogLevel))
 | 
							log.Info().Str("version", assets.Version).Msg("Starting tinyauth")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Users
 | 
							// Users
 | 
				
			||||||
		log.Info().Msg("Parsing users")
 | 
							log.Info().Msg("Parsing users")
 | 
				
			||||||
@@ -46,36 +58,38 @@ var rootCmd = &cobra.Command{
 | 
				
			|||||||
			log.Fatal().Err(usersErr).Msg("Failed to parse users")
 | 
								log.Fatal().Err(usersErr).Msg("Failed to parse users")
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Secrets
 | 
					 | 
				
			||||||
		log.Info().Msg("Parsing secrets")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		config.Secret = utils.GetSecret(config.Secret, config.SecretFile)
 | 
					 | 
				
			||||||
		config.GithubClientSecret = utils.GetSecret(config.GithubClientSecret, config.GithubClientSecretFile)
 | 
					 | 
				
			||||||
		config.GoogleClientSecret = utils.GetSecret(config.GoogleClientSecret, config.GoogleClientSecretFile)
 | 
					 | 
				
			||||||
		config.GenericClientSecret = utils.GetSecret(config.GenericClientSecret, config.GenericClientSecretFile)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Create oauth whitelist
 | 
							// Create oauth whitelist
 | 
				
			||||||
		oauthWhitelist := strings.Split(config.OAuthWhitelist, ",")
 | 
							oauthWhitelist := strings.Split(config.OAuthWhitelist, ",")
 | 
				
			||||||
		log.Debug().Strs("oauth_whitelist", oauthWhitelist).Msg("Parsed OAuth whitelist")
 | 
							log.Debug().Msg("Parsed OAuth whitelist")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Create OAuth config
 | 
							// Create OAuth config
 | 
				
			||||||
		oauthConfig := types.OAuthConfig{
 | 
							oauthConfig := types.OAuthConfig{
 | 
				
			||||||
			GithubClientId:      config.GithubClientId,
 | 
								GithubClientId:        config.GithubClientId,
 | 
				
			||||||
			GithubClientSecret:  config.GithubClientSecret,
 | 
								GithubClientSecret:    config.GithubClientSecret,
 | 
				
			||||||
			GoogleClientId:      config.GoogleClientId,
 | 
								GoogleClientId:        config.GoogleClientId,
 | 
				
			||||||
			GoogleClientSecret:  config.GoogleClientSecret,
 | 
								GoogleClientSecret:    config.GoogleClientSecret,
 | 
				
			||||||
			GenericClientId:     config.GenericClientId,
 | 
								TailscaleClientId:     config.TailscaleClientId,
 | 
				
			||||||
			GenericClientSecret: config.GenericClientSecret,
 | 
								TailscaleClientSecret: config.TailscaleClientSecret,
 | 
				
			||||||
			GenericScopes:       strings.Split(config.GenericScopes, ","),
 | 
								GenericClientId:       config.GenericClientId,
 | 
				
			||||||
			GenericAuthURL:      config.GenericAuthURL,
 | 
								GenericClientSecret:   config.GenericClientSecret,
 | 
				
			||||||
			GenericTokenURL:     config.GenericTokenURL,
 | 
								GenericScopes:         strings.Split(config.GenericScopes, ","),
 | 
				
			||||||
			GenericUserURL:      config.GenericUserURL,
 | 
								GenericAuthURL:        config.GenericAuthURL,
 | 
				
			||||||
			AppURL:              config.AppURL,
 | 
								GenericTokenURL:       config.GenericTokenURL,
 | 
				
			||||||
 | 
								GenericUserURL:        config.GenericUserURL,
 | 
				
			||||||
 | 
								AppURL:                config.AppURL,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		log.Debug().Interface("oauth_config", oauthConfig).Msg("Parsed OAuth config")
 | 
					
 | 
				
			||||||
 | 
							log.Debug().Msg("Parsed OAuth config")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Create docker service
 | 
				
			||||||
 | 
							docker := docker.NewDocker()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Initialize docker
 | 
				
			||||||
 | 
							dockerErr := docker.Init()
 | 
				
			||||||
 | 
							HandleError(dockerErr, "Failed to initialize docker")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Create auth service
 | 
							// Create auth service
 | 
				
			||||||
		auth := auth.NewAuth(users, oauthWhitelist)
 | 
							auth := auth.NewAuth(docker, users, oauthWhitelist)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Create OAuth providers service
 | 
							// Create OAuth providers service
 | 
				
			||||||
		providers := providers.NewProviders(oauthConfig)
 | 
							providers := providers.NewProviders(oauthConfig)
 | 
				
			||||||
@@ -136,6 +150,9 @@ func init() {
 | 
				
			|||||||
	rootCmd.Flags().String("google-client-id", "", "Google OAuth client ID.")
 | 
						rootCmd.Flags().String("google-client-id", "", "Google OAuth client ID.")
 | 
				
			||||||
	rootCmd.Flags().String("google-client-secret", "", "Google OAuth client secret.")
 | 
						rootCmd.Flags().String("google-client-secret", "", "Google OAuth client secret.")
 | 
				
			||||||
	rootCmd.Flags().String("google-client-secret-file", "", "Google OAuth client secret file.")
 | 
						rootCmd.Flags().String("google-client-secret-file", "", "Google OAuth client secret file.")
 | 
				
			||||||
 | 
						rootCmd.Flags().String("tailscale-client-id", "", "Tailscale OAuth client ID.")
 | 
				
			||||||
 | 
						rootCmd.Flags().String("tailscale-client-secret", "", "Tailscale OAuth client secret.")
 | 
				
			||||||
 | 
						rootCmd.Flags().String("tailscale-client-secret-file", "", "Tailscale OAuth client secret file.")
 | 
				
			||||||
	rootCmd.Flags().String("generic-client-id", "", "Generic OAuth client ID.")
 | 
						rootCmd.Flags().String("generic-client-id", "", "Generic OAuth client ID.")
 | 
				
			||||||
	rootCmd.Flags().String("generic-client-secret", "", "Generic OAuth client secret.")
 | 
						rootCmd.Flags().String("generic-client-secret", "", "Generic OAuth client secret.")
 | 
				
			||||||
	rootCmd.Flags().String("generic-client-secret-file", "", "Generic OAuth client secret file.")
 | 
						rootCmd.Flags().String("generic-client-secret-file", "", "Generic OAuth client secret file.")
 | 
				
			||||||
@@ -161,6 +178,9 @@ func init() {
 | 
				
			|||||||
	viper.BindEnv("google-client-id", "GOOGLE_CLIENT_ID")
 | 
						viper.BindEnv("google-client-id", "GOOGLE_CLIENT_ID")
 | 
				
			||||||
	viper.BindEnv("google-client-secret", "GOOGLE_CLIENT_SECRET")
 | 
						viper.BindEnv("google-client-secret", "GOOGLE_CLIENT_SECRET")
 | 
				
			||||||
	viper.BindEnv("google-client-secret-file", "GOOGLE_CLIENT_SECRET_FILE")
 | 
						viper.BindEnv("google-client-secret-file", "GOOGLE_CLIENT_SECRET_FILE")
 | 
				
			||||||
 | 
						viper.BindEnv("tailscale-client-id", "TAILSCALE_CLIENT_ID")
 | 
				
			||||||
 | 
						viper.BindEnv("tailscale-client-secret", "TAILSCALE_CLIENT_SECRET")
 | 
				
			||||||
 | 
						viper.BindEnv("tailscale-client-secret-file", "TAILSCALE_CLIENT_SECRET_FILE")
 | 
				
			||||||
	viper.BindEnv("generic-client-id", "GENERIC_CLIENT_ID")
 | 
						viper.BindEnv("generic-client-id", "GENERIC_CLIENT_ID")
 | 
				
			||||||
	viper.BindEnv("generic-client-secret", "GENERIC_CLIENT_SECRET")
 | 
						viper.BindEnv("generic-client-secret", "GENERIC_CLIENT_SECRET")
 | 
				
			||||||
	viper.BindEnv("generic-client-secret-file", "GENERIC_CLIENT_SECRET_FILE")
 | 
						viper.BindEnv("generic-client-secret-file", "GENERIC_CLIENT_SECRET_FILE")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,7 @@ import (
 | 
				
			|||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/charmbracelet/huh"
 | 
						"github.com/charmbracelet/huh"
 | 
				
			||||||
 | 
						"github.com/rs/zerolog"
 | 
				
			||||||
	"github.com/rs/zerolog/log"
 | 
						"github.com/rs/zerolog/log"
 | 
				
			||||||
	"github.com/spf13/cobra"
 | 
						"github.com/spf13/cobra"
 | 
				
			||||||
	"golang.org/x/crypto/bcrypt"
 | 
						"golang.org/x/crypto/bcrypt"
 | 
				
			||||||
@@ -21,6 +22,8 @@ var CreateCmd = &cobra.Command{
 | 
				
			|||||||
	Short: "Create a user",
 | 
						Short: "Create a user",
 | 
				
			||||||
	Long:  `Create a user either interactively or by passing flags.`,
 | 
						Long:  `Create a user either interactively or by passing flags.`,
 | 
				
			||||||
	Run: func(cmd *cobra.Command, args []string) {
 | 
						Run: func(cmd *cobra.Command, args []string) {
 | 
				
			||||||
 | 
							log.Logger = log.Level(zerolog.InfoLevel)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if interactive {
 | 
							if interactive {
 | 
				
			||||||
			form := huh.NewForm(
 | 
								form := huh.NewForm(
 | 
				
			||||||
				huh.NewGroup(
 | 
									huh.NewGroup(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,12 +8,17 @@ import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func UserCmd() *cobra.Command {
 | 
					func UserCmd() *cobra.Command {
 | 
				
			||||||
 | 
						// Create the user command
 | 
				
			||||||
	userCmd := &cobra.Command{
 | 
						userCmd := &cobra.Command{
 | 
				
			||||||
		Use:  "user",
 | 
							Use:   "user",
 | 
				
			||||||
		Short: "User utilities",
 | 
							Short: "User utilities",
 | 
				
			||||||
		Long: `Utilities for creating and verifying tinyauth compatible users.`,
 | 
							Long:  `Utilities for creating and verifying tinyauth compatible users.`,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Add subcommands
 | 
				
			||||||
	userCmd.AddCommand(create.CreateCmd)
 | 
						userCmd.AddCommand(create.CreateCmd)
 | 
				
			||||||
	userCmd.AddCommand(verify.VerifyCmd)
 | 
						userCmd.AddCommand(verify.VerifyCmd)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Return the user command
 | 
				
			||||||
	return userCmd
 | 
						return userCmd
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -5,6 +5,7 @@ import (
 | 
				
			|||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/charmbracelet/huh"
 | 
						"github.com/charmbracelet/huh"
 | 
				
			||||||
 | 
						"github.com/rs/zerolog"
 | 
				
			||||||
	"github.com/rs/zerolog/log"
 | 
						"github.com/rs/zerolog/log"
 | 
				
			||||||
	"github.com/spf13/cobra"
 | 
						"github.com/spf13/cobra"
 | 
				
			||||||
	"golang.org/x/crypto/bcrypt"
 | 
						"golang.org/x/crypto/bcrypt"
 | 
				
			||||||
@@ -21,6 +22,8 @@ var VerifyCmd = &cobra.Command{
 | 
				
			|||||||
	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 and password.`,
 | 
				
			||||||
	Run: func(cmd *cobra.Command, args []string) {
 | 
						Run: func(cmd *cobra.Command, args []string) {
 | 
				
			||||||
 | 
							log.Logger = log.Level(zerolog.InfoLevel)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if interactive {
 | 
							if interactive {
 | 
				
			||||||
			form := huh.NewForm(
 | 
								form := huh.NewForm(
 | 
				
			||||||
				huh.NewGroup(
 | 
									huh.NewGroup(
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										17
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								go.mod
									
									
									
									
									
								
							@@ -14,6 +14,7 @@ require (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require (
 | 
					require (
 | 
				
			||||||
 | 
						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/bytedance/sonic v1.12.7 // indirect
 | 
						github.com/bytedance/sonic v1.12.7 // indirect
 | 
				
			||||||
@@ -27,14 +28,22 @@ require (
 | 
				
			|||||||
	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/docker/docker v27.5.1+incompatible // indirect
 | 
				
			||||||
 | 
						github.com/docker/go-connections 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
 | 
				
			||||||
	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
 | 
						github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
 | 
				
			||||||
 | 
						github.com/felixge/httpsnoop v1.0.4 // indirect
 | 
				
			||||||
	github.com/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/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
 | 
				
			||||||
	github.com/goccy/go-json v0.10.4 // indirect
 | 
						github.com/goccy/go-json v0.10.4 // indirect
 | 
				
			||||||
 | 
						github.com/gogo/protobuf v1.3.2 // indirect
 | 
				
			||||||
	github.com/gorilla/context v1.1.2 // indirect
 | 
						github.com/gorilla/context v1.1.2 // indirect
 | 
				
			||||||
	github.com/gorilla/securecookie v1.1.2 // indirect
 | 
						github.com/gorilla/securecookie v1.1.2 // indirect
 | 
				
			||||||
	github.com/gorilla/sessions v1.2.2 // indirect
 | 
						github.com/gorilla/sessions v1.2.2 // indirect
 | 
				
			||||||
@@ -51,12 +60,16 @@ require (
 | 
				
			|||||||
	github.com/mattn/go-runewidth v0.0.16 // indirect
 | 
						github.com/mattn/go-runewidth v0.0.16 // indirect
 | 
				
			||||||
	github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
 | 
						github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
 | 
				
			||||||
	github.com/mitchellh/mapstructure v1.5.0 // indirect
 | 
						github.com/mitchellh/mapstructure v1.5.0 // indirect
 | 
				
			||||||
 | 
						github.com/moby/docker-image-spec v1.3.1 // indirect
 | 
				
			||||||
	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 | 
						github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 | 
				
			||||||
	github.com/modern-go/reflect2 v1.0.2 // indirect
 | 
						github.com/modern-go/reflect2 v1.0.2 // indirect
 | 
				
			||||||
	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
 | 
						github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
 | 
				
			||||||
	github.com/muesli/cancelreader v0.2.2 // indirect
 | 
						github.com/muesli/cancelreader v0.2.2 // indirect
 | 
				
			||||||
	github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect
 | 
						github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect
 | 
				
			||||||
 | 
						github.com/opencontainers/go-digest v1.0.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/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
 | 
				
			||||||
@@ -67,6 +80,10 @@ require (
 | 
				
			|||||||
	github.com/subosito/gotenv v1.6.0 // indirect
 | 
						github.com/subosito/gotenv v1.6.0 // indirect
 | 
				
			||||||
	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/otel v1.24.0 // indirect
 | 
				
			||||||
 | 
						go.opentelemetry.io/otel/metric v1.24.0 // indirect
 | 
				
			||||||
 | 
						go.opentelemetry.io/otel/trace v1.24.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
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										69
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										69
									
								
								go.sum
									
									
									
									
									
								
							@@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					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/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=
 | 
				
			||||||
@@ -32,10 +34,20 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 | 
				
			|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
					github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
				
			||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 | 
					github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 | 
				
			||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
					github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
				
			||||||
 | 
					github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
 | 
				
			||||||
 | 
					github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
 | 
				
			||||||
 | 
					github.com/docker/docker v27.5.1+incompatible h1:4PYU5dnBYqRQi0294d1FBECqT9ECWeQAIfE8q4YnPY8=
 | 
				
			||||||
 | 
					github.com/docker/docker v27.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 | 
				
			||||||
 | 
					github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
 | 
				
			||||||
 | 
					github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
 | 
				
			||||||
 | 
					github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
 | 
				
			||||||
 | 
					github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
 | 
				
			||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 | 
					github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 | 
				
			||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 | 
					github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 | 
				
			||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
 | 
					github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
 | 
				
			||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
 | 
					github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
 | 
				
			||||||
 | 
					github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 | 
				
			||||||
 | 
					github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 | 
				
			||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
 | 
					github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
 | 
				
			||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 | 
					github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 | 
				
			||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
 | 
					github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
 | 
				
			||||||
@@ -48,6 +60,11 @@ github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E
 | 
				
			|||||||
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
 | 
					github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
 | 
				
			||||||
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.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
 | 
				
			||||||
 | 
					github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 | 
				
			||||||
 | 
					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-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
 | 
					github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
 | 
				
			||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 | 
					github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 | 
				
			||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
 | 
					github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
 | 
				
			||||||
@@ -59,6 +76,8 @@ github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1
 | 
				
			|||||||
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
 | 
					github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
 | 
				
			||||||
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
 | 
					github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
 | 
				
			||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 | 
					github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 | 
				
			||||||
 | 
					github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 | 
				
			||||||
 | 
					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.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 | 
				
			||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 | 
					github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 | 
				
			||||||
@@ -79,10 +98,13 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
 | 
				
			|||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 | 
					github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 | 
				
			||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 | 
					github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 | 
				
			||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 | 
					github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 | 
				
			||||||
 | 
					github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 | 
				
			||||||
 | 
					github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 | 
				
			||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 | 
					github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 | 
				
			||||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
 | 
					github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
 | 
				
			||||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
 | 
					github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
 | 
				
			||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
 | 
					github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
 | 
				
			||||||
 | 
					github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 | 
				
			||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 | 
					github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 | 
				
			||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 | 
					github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 | 
				
			||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 | 
					github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 | 
				
			||||||
@@ -108,6 +130,8 @@ github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4
 | 
				
			|||||||
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/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
 | 
				
			||||||
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=
 | 
				
			||||||
@@ -119,8 +143,14 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
 | 
				
			|||||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
 | 
					github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
 | 
				
			||||||
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg=
 | 
					github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg=
 | 
				
			||||||
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=
 | 
					github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=
 | 
				
			||||||
 | 
					github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 | 
				
			||||||
 | 
					github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 | 
				
			||||||
 | 
					github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
 | 
				
			||||||
 | 
					github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
 | 
				
			||||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
 | 
					github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
 | 
				
			||||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
 | 
					github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
 | 
				
			||||||
 | 
					github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 | 
				
			||||||
 | 
					github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 | 
				
			||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 | 
					github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 | 
				
			||||||
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=
 | 
				
			||||||
@@ -138,6 +168,7 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke
 | 
				
			|||||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
 | 
					github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
 | 
				
			||||||
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/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=
 | 
				
			||||||
@@ -151,9 +182,11 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
 | 
				
			|||||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
 | 
					github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
 | 
				
			||||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
 | 
					github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
 | 
				
			||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
					github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
				
			||||||
 | 
					github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
				
			||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 | 
					github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 | 
				
			||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 | 
					github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 | 
				
			||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 | 
					github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 | 
				
			||||||
 | 
					github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 | 
				
			||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 | 
					github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 | 
				
			||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
					github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
				
			||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
					github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
				
			||||||
@@ -168,31 +201,67 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
 | 
				
			|||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
 | 
					github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
 | 
				
			||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
 | 
					github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
 | 
				
			||||||
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.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 | 
				
			||||||
 | 
					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/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
 | 
				
			||||||
 | 
					go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
 | 
				
			||||||
 | 
					go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
 | 
				
			||||||
 | 
					go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
 | 
				
			||||||
 | 
					go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
 | 
				
			||||||
 | 
					go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
 | 
				
			||||||
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=
 | 
				
			||||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
 | 
					go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
 | 
				
			||||||
golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
 | 
					golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
 | 
				
			||||||
golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
 | 
					golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
 | 
				
			||||||
 | 
					golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
				
			||||||
 | 
					golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
				
			||||||
 | 
					golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 | 
				
			||||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
 | 
					golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
 | 
				
			||||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
 | 
					golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
 | 
				
			||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
 | 
					golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
 | 
				
			||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
 | 
					golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
 | 
				
			||||||
 | 
					golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 | 
				
			||||||
 | 
					golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 | 
				
			||||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
 | 
					golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
 | 
				
			||||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
 | 
					golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
 | 
				
			||||||
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
 | 
					golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
 | 
				
			||||||
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 | 
					golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 | 
				
			||||||
 | 
					golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
 | 
					golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
 | 
					golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
 | 
					golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
 | 
				
			||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 | 
					golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
					golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
				
			||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
					golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
				
			||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
					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/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.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/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-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 | 
				
			||||||
 | 
					golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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=
 | 
				
			||||||
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=
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@ package api
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io/fs"
 | 
						"io/fs"
 | 
				
			||||||
 | 
						"math/rand/v2"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
@@ -98,8 +99,30 @@ func (api *API) SetupRoutes() {
 | 
				
			|||||||
		log.Debug().Msg("Checking auth")
 | 
							log.Debug().Msg("Checking auth")
 | 
				
			||||||
		userContext := api.Hooks.UseUserContext(c)
 | 
							userContext := api.Hooks.UseUserContext(c)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							uri := c.Request.Header.Get("X-Forwarded-Uri")
 | 
				
			||||||
 | 
							proto := c.Request.Header.Get("X-Forwarded-Proto")
 | 
				
			||||||
 | 
							host := c.Request.Header.Get("X-Forwarded-Host")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if userContext.IsLoggedIn {
 | 
							if userContext.IsLoggedIn {
 | 
				
			||||||
			log.Debug().Msg("Authenticated")
 | 
								log.Debug().Msg("Authenticated")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								appAllowed, appAllowedErr := api.Auth.ResourceAllowed(userContext, host)
 | 
				
			||||||
 | 
								if handleApiError(c, "Failed to check if resource is allowed", appAllowedErr) {
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if !appAllowed {
 | 
				
			||||||
 | 
									log.Warn().Str("username", userContext.Username).Str("host", host).Msg("User not allowed")
 | 
				
			||||||
 | 
									queries, queryErr := query.Values(types.UnauthorizedQuery{
 | 
				
			||||||
 | 
										Username: userContext.Username,
 | 
				
			||||||
 | 
										Resource: strings.Split(host, ".")[0],
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
									if handleApiError(c, "Failed to build query", queryErr) {
 | 
				
			||||||
 | 
										return
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", api.Config.AppURL, queries.Encode()))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			c.JSON(200, gin.H{
 | 
								c.JSON(200, gin.H{
 | 
				
			||||||
				"status":  200,
 | 
									"status":  200,
 | 
				
			||||||
				"message": "Authenticated",
 | 
									"message": "Authenticated",
 | 
				
			||||||
@@ -107,14 +130,11 @@ func (api *API) SetupRoutes() {
 | 
				
			|||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		uri := c.Request.Header.Get("X-Forwarded-Uri")
 | 
					 | 
				
			||||||
		proto := c.Request.Header.Get("X-Forwarded-Proto")
 | 
					 | 
				
			||||||
		host := c.Request.Header.Get("X-Forwarded-Host")
 | 
					 | 
				
			||||||
		queries, queryErr := query.Values(types.LoginQuery{
 | 
							queries, queryErr := query.Values(types.LoginQuery{
 | 
				
			||||||
			RedirectURI: fmt.Sprintf("%s://%s%s", proto, host, uri),
 | 
								RedirectURI: fmt.Sprintf("%s://%s%s", proto, host, uri),
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		log.Debug().Interface("queries", queries).Msg("Redirecting to login")
 | 
							log.Debug().Interface("redirect_uri", fmt.Sprintf("%s://%s%s", proto, host, uri)).Msg("Redirecting to login")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if queryErr != nil {
 | 
							if queryErr != nil {
 | 
				
			||||||
			log.Error().Err(queryErr).Msg("Failed to build query")
 | 
								log.Error().Err(queryErr).Msg("Failed to build query")
 | 
				
			||||||
@@ -142,7 +162,7 @@ func (api *API) SetupRoutes() {
 | 
				
			|||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		log.Debug().Interface("login", login).Msg("Got login request")
 | 
							log.Debug().Msg("Got login request")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		user := api.Auth.GetUser(login.Username)
 | 
							user := api.Auth.GetUser(login.Username)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -250,7 +270,7 @@ func (api *API) SetupRoutes() {
 | 
				
			|||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		log.Debug().Interface("request", request).Msg("Got OAuth request")
 | 
							log.Debug().Msg("Got OAuth request")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		provider := api.Providers.GetProvider(request.Provider)
 | 
							provider := api.Providers.GetProvider(request.Provider)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -266,7 +286,7 @@ func (api *API) SetupRoutes() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		authURL := provider.GetAuthURL()
 | 
							authURL := provider.GetAuthURL()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		log.Debug().Str("authURL", authURL).Msg("Got auth URL")
 | 
							log.Debug().Msg("Got auth URL")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		redirectURI := c.Query("redirect_uri")
 | 
							redirectURI := c.Query("redirect_uri")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -275,6 +295,21 @@ func (api *API) SetupRoutes() {
 | 
				
			|||||||
			c.SetCookie("tinyauth_redirect_uri", redirectURI, 3600, "/", api.Domain, api.Config.CookieSecure, true)
 | 
								c.SetCookie("tinyauth_redirect_uri", redirectURI, 3600, "/", api.Domain, api.Config.CookieSecure, true)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if request.Provider == "tailscale" {
 | 
				
			||||||
 | 
								tailscaleQuery, tailscaleQueryErr := query.Values(types.TailscaleQuery{
 | 
				
			||||||
 | 
									Code: (1000 + rand.IntN(9000)), // doesn't need to be secure, just there to avoid caching
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								if handleApiError(c, "Failed to build query", tailscaleQueryErr) {
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								c.JSON(200, gin.H{
 | 
				
			||||||
 | 
									"status":  200,
 | 
				
			||||||
 | 
									"message": "Ok",
 | 
				
			||||||
 | 
									"url":     fmt.Sprintf("%s/api/oauth/callback/tailscale?%s", api.Config.AppURL, tailscaleQuery.Encode()),
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		c.JSON(200, gin.H{
 | 
							c.JSON(200, gin.H{
 | 
				
			||||||
			"status":  200,
 | 
								"status":  200,
 | 
				
			||||||
			"message": "Ok",
 | 
								"message": "Ok",
 | 
				
			||||||
@@ -291,7 +326,7 @@ func (api *API) SetupRoutes() {
 | 
				
			|||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		log.Debug().Interface("providerName", providerName).Msg("Got provider name")
 | 
							log.Debug().Interface("provider", providerName.Provider).Msg("Got provider name")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		code := c.Query("code")
 | 
							code := c.Query("code")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -301,7 +336,7 @@ func (api *API) SetupRoutes() {
 | 
				
			|||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		log.Debug().Str("code", code).Msg("Got code")
 | 
							log.Debug().Msg("Got code")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		provider := api.Providers.GetProvider(providerName.Provider)
 | 
							provider := api.Providers.GetProvider(providerName.Provider)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -312,9 +347,9 @@ func (api *API) SetupRoutes() {
 | 
				
			|||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		token, tokenErr := provider.ExchangeToken(code)
 | 
							_, tokenErr := provider.ExchangeToken(code)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		log.Debug().Str("token", token).Msg("Got token")
 | 
							log.Debug().Msg("Got token")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if handleApiError(c, "Failed to exchange token", tokenErr) {
 | 
							if handleApiError(c, "Failed to exchange token", tokenErr) {
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
@@ -363,7 +398,7 @@ func (api *API) SetupRoutes() {
 | 
				
			|||||||
			RedirectURI: redirectURI,
 | 
								RedirectURI: redirectURI,
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		log.Debug().Interface("redirectQuery", redirectQuery).Msg("Got redirect query")
 | 
							log.Debug().Msg("Got redirect query")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if handleApiError(c, "Failed to build query", redirectQueryErr) {
 | 
							if handleApiError(c, "Failed to build query", redirectQueryErr) {
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1 +1 @@
 | 
				
			|||||||
v2.0.0
 | 
					v2.1.0
 | 
				
			||||||
@@ -1,7 +1,11 @@
 | 
				
			|||||||
package auth
 | 
					package auth
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"slices"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"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"
 | 
				
			||||||
@@ -9,8 +13,9 @@ import (
 | 
				
			|||||||
	"golang.org/x/crypto/bcrypt"
 | 
						"golang.org/x/crypto/bcrypt"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func NewAuth(userList types.Users, oauthWhitelist []string) *Auth {
 | 
					func NewAuth(docker *docker.Docker, userList types.Users, oauthWhitelist []string) *Auth {
 | 
				
			||||||
	return &Auth{
 | 
						return &Auth{
 | 
				
			||||||
 | 
							Docker:         docker,
 | 
				
			||||||
		Users:          userList,
 | 
							Users:          userList,
 | 
				
			||||||
		OAuthWhitelist: oauthWhitelist,
 | 
							OAuthWhitelist: oauthWhitelist,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -18,6 +23,7 @@ func NewAuth(userList types.Users, oauthWhitelist []string) *Auth {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
type Auth struct {
 | 
					type Auth struct {
 | 
				
			||||||
	Users          types.Users
 | 
						Users          types.Users
 | 
				
			||||||
 | 
						Docker         *docker.Docker
 | 
				
			||||||
	OAuthWhitelist []string
 | 
						OAuthWhitelist []string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -50,7 +56,7 @@ func (auth *Auth) EmailWhitelisted(emailSrc string) bool {
 | 
				
			|||||||
func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) {
 | 
					func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) {
 | 
				
			||||||
	log.Debug().Msg("Creating session cookie")
 | 
						log.Debug().Msg("Creating session cookie")
 | 
				
			||||||
	sessions := sessions.Default(c)
 | 
						sessions := sessions.Default(c)
 | 
				
			||||||
	log.Debug().Interface("data", data).Msg("Setting session cookie")
 | 
						log.Debug().Msg("Setting session cookie")
 | 
				
			||||||
	sessions.Set("username", data.Username)
 | 
						sessions.Set("username", data.Username)
 | 
				
			||||||
	sessions.Set("provider", data.Provider)
 | 
						sessions.Set("provider", data.Provider)
 | 
				
			||||||
	sessions.Save()
 | 
						sessions.Save()
 | 
				
			||||||
@@ -70,13 +76,10 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error)
 | 
				
			|||||||
	cookieUsername := sessions.Get("username")
 | 
						cookieUsername := sessions.Get("username")
 | 
				
			||||||
	cookieProvider := sessions.Get("provider")
 | 
						cookieProvider := sessions.Get("provider")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	log.Debug().Interface("cookieUsername", cookieUsername).Msg("Got username")
 | 
					 | 
				
			||||||
	log.Debug().Interface("cookieProvider", cookieProvider).Msg("Got provider")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	username, usernameOk := cookieUsername.(string)
 | 
						username, usernameOk := cookieUsername.(string)
 | 
				
			||||||
	provider, providerOk := cookieProvider.(string)
 | 
						provider, providerOk := cookieProvider.(string)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	log.Debug().Str("username", username).Bool("usernameOk", usernameOk).Str("provider", provider).Bool("providerOk", providerOk).Msg("Parsed cookie")
 | 
						log.Debug().Str("username", username).Str("provider", provider).Msg("Parsed cookie")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if !usernameOk || !providerOk {
 | 
						if !usernameOk || !providerOk {
 | 
				
			||||||
		log.Warn().Msg("Session cookie invalid")
 | 
							log.Warn().Msg("Session cookie invalid")
 | 
				
			||||||
@@ -92,3 +95,53 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error)
 | 
				
			|||||||
func (auth *Auth) UserAuthConfigured() bool {
 | 
					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) {
 | 
				
			||||||
 | 
						appId := strings.Split(host, ".")[0]
 | 
				
			||||||
 | 
						containers, containersErr := auth.Docker.GetContainers()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if containersErr != nil {
 | 
				
			||||||
 | 
							return false, containersErr
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						log.Debug().Msg("Got containers")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, container := range containers {
 | 
				
			||||||
 | 
							inspect, inspectErr := auth.Docker.InspectContainer(container.ID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if inspectErr != nil {
 | 
				
			||||||
 | 
								return false, inspectErr
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							containerName := strings.Split(inspect.Name, "/")[1]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if containerName == appId {
 | 
				
			||||||
 | 
								log.Debug().Str("container", containerName).Msg("Found container")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								labels := utils.GetTinyauthLabels(inspect.Config.Labels)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								log.Debug().Msg("Got labels")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if context.OAuth && len(labels.OAuthWhitelist) != 0 {
 | 
				
			||||||
 | 
									log.Debug().Msg("Checking OAuth whitelist")
 | 
				
			||||||
 | 
									if slices.Contains(labels.OAuthWhitelist, context.Username) {
 | 
				
			||||||
 | 
										return true, nil
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									return false, nil
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if len(labels.Users) != 0 {
 | 
				
			||||||
 | 
									log.Debug().Msg("Checking users")
 | 
				
			||||||
 | 
									if slices.Contains(labels.Users, context.Username) {
 | 
				
			||||||
 | 
										return true, nil
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									return false, nil
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						log.Debug().Msg("No matching container found, allowing access")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return true, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										6
									
								
								internal/constants/constants.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								internal/constants/constants.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					package constants
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var TinyauthLabels = []string{
 | 
				
			||||||
 | 
						"tinyauth.oauth.whitelist",
 | 
				
			||||||
 | 
						"tinyauth.users",
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										51
									
								
								internal/docker/docker.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								internal/docker/docker.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
				
			|||||||
 | 
					package docker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/docker/docker/api/types"
 | 
				
			||||||
 | 
						"github.com/docker/docker/api/types/container"
 | 
				
			||||||
 | 
						"github.com/docker/docker/client"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NewDocker() *Docker {
 | 
				
			||||||
 | 
						return &Docker{}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Docker struct {
 | 
				
			||||||
 | 
						Client  *client.Client
 | 
				
			||||||
 | 
						Context context.Context
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (docker *Docker) Init() error {
 | 
				
			||||||
 | 
						apiClient, err := client.NewClientWithOpts(client.FromEnv)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						docker.Context = context.Background()
 | 
				
			||||||
 | 
						docker.Client = apiClient
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (docker *Docker) GetContainers() ([]types.Container, error) {
 | 
				
			||||||
 | 
						containers, err := docker.Client.ContainerList(docker.Context, container.ListOptions{})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return containers, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (docker *Docker) InspectContainer(containerId string) (types.ContainerJSON, error) {
 | 
				
			||||||
 | 
						inspect, err := docker.Client.ContainerInspect(docker.Context, containerId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return types.ContainerJSON{}, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return inspect, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -34,8 +34,6 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	log.Debug().Interface("cookie", cookie).Msg("Got session cookie")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if cookie.Provider == "username" {
 | 
						if cookie.Provider == "username" {
 | 
				
			||||||
		log.Debug().Msg("Provider is username")
 | 
							log.Debug().Msg("Provider is username")
 | 
				
			||||||
		if hooks.Auth.GetUser(cookie.Username) != nil {
 | 
							if hooks.Auth.GetUser(cookie.Username) != nil {
 | 
				
			||||||
@@ -55,7 +53,7 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
 | 
				
			|||||||
	if provider != nil {
 | 
						if provider != nil {
 | 
				
			||||||
		log.Debug().Msg("Provider exists")
 | 
							log.Debug().Msg("Provider exists")
 | 
				
			||||||
		if !hooks.Auth.EmailWhitelisted(cookie.Username) {
 | 
							if !hooks.Auth.EmailWhitelisted(cookie.Username) {
 | 
				
			||||||
			log.Error().Msgf("Email %s not whitelisted", cookie.Username)
 | 
								log.Error().Str("email", cookie.Username).Msg("Email is not whitelisted")
 | 
				
			||||||
			hooks.Auth.DeleteSessionCookie(c)
 | 
								hooks.Auth.DeleteSessionCookie(c)
 | 
				
			||||||
			return types.UserContext{
 | 
								return types.UserContext{
 | 
				
			||||||
				Username:   "",
 | 
									Username:   "",
 | 
				
			||||||
@@ -73,7 +71,6 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	log.Error().Msg("Provider does not exist")
 | 
					 | 
				
			||||||
	return types.UserContext{
 | 
						return types.UserContext{
 | 
				
			||||||
		Username:   "",
 | 
							Username:   "",
 | 
				
			||||||
		IsLoggedIn: false,
 | 
							IsLoggedIn: false,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -37,7 +37,7 @@ func GetGenericEmail(client *http.Client, url string) (string, error) {
 | 
				
			|||||||
		return "", jsonErr
 | 
							return "", jsonErr
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	log.Debug().Interface("user", user).Msg("Parsed user from generic provider")
 | 
						log.Debug().Msg("Parsed user from generic provider")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return user.Email, nil
 | 
						return user.Email, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -43,7 +43,7 @@ func GetGithubEmail(client *http.Client) (string, error) {
 | 
				
			|||||||
		return "", jsonErr
 | 
							return "", jsonErr
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	log.Debug().Interface("emails", emails).Msg("Parsed emails from github")
 | 
						log.Debug().Msg("Parsed emails from github")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, email := range emails {
 | 
						for _, email := range emails {
 | 
				
			||||||
		if email.Primary {
 | 
							if email.Primary {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -41,7 +41,7 @@ func GetGoogleEmail(client *http.Client) (string, error) {
 | 
				
			|||||||
		return "", jsonErr
 | 
							return "", jsonErr
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	log.Debug().Interface("user", user).Msg("Parsed user from google")
 | 
						log.Debug().Msg("Parsed user from google")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return user.Email, nil
 | 
						return user.Email, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,10 +17,11 @@ func NewProviders(config types.OAuthConfig) *Providers {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Providers struct {
 | 
					type Providers struct {
 | 
				
			||||||
	Config  types.OAuthConfig
 | 
						Config    types.OAuthConfig
 | 
				
			||||||
	Github  *oauth.OAuth
 | 
						Github    *oauth.OAuth
 | 
				
			||||||
	Google  *oauth.OAuth
 | 
						Google    *oauth.OAuth
 | 
				
			||||||
	Generic *oauth.OAuth
 | 
						Tailscale *oauth.OAuth
 | 
				
			||||||
 | 
						Generic   *oauth.OAuth
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (providers *Providers) Init() {
 | 
					func (providers *Providers) Init() {
 | 
				
			||||||
@@ -46,6 +47,17 @@ func (providers *Providers) Init() {
 | 
				
			|||||||
		})
 | 
							})
 | 
				
			||||||
		providers.Google.Init()
 | 
							providers.Google.Init()
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						if providers.Config.TailscaleClientId != "" && providers.Config.TailscaleClientSecret != "" {
 | 
				
			||||||
 | 
							log.Info().Msg("Initializing Tailscale OAuth")
 | 
				
			||||||
 | 
							providers.Tailscale = oauth.NewOAuth(oauth2.Config{
 | 
				
			||||||
 | 
								ClientID:     providers.Config.TailscaleClientId,
 | 
				
			||||||
 | 
								ClientSecret: providers.Config.TailscaleClientSecret,
 | 
				
			||||||
 | 
								RedirectURL:  fmt.Sprintf("%s/api/oauth/callback/tailscale", providers.Config.AppURL),
 | 
				
			||||||
 | 
								Scopes:       TailscaleScopes(),
 | 
				
			||||||
 | 
								Endpoint:     TailscaleEndpoint,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							providers.Tailscale.Init()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	if providers.Config.GenericClientId != "" && providers.Config.GenericClientSecret != "" {
 | 
						if providers.Config.GenericClientId != "" && providers.Config.GenericClientSecret != "" {
 | 
				
			||||||
		log.Info().Msg("Initializing Generic OAuth")
 | 
							log.Info().Msg("Initializing Generic OAuth")
 | 
				
			||||||
		providers.Generic = oauth.NewOAuth(oauth2.Config{
 | 
							providers.Generic = oauth.NewOAuth(oauth2.Config{
 | 
				
			||||||
@@ -68,6 +80,8 @@ func (providers *Providers) GetProvider(provider string) *oauth.OAuth {
 | 
				
			|||||||
		return providers.Github
 | 
							return providers.Github
 | 
				
			||||||
	case "google":
 | 
						case "google":
 | 
				
			||||||
		return providers.Google
 | 
							return providers.Google
 | 
				
			||||||
 | 
						case "tailscale":
 | 
				
			||||||
 | 
							return providers.Tailscale
 | 
				
			||||||
	case "generic":
 | 
						case "generic":
 | 
				
			||||||
		return providers.Generic
 | 
							return providers.Generic
 | 
				
			||||||
	default:
 | 
						default:
 | 
				
			||||||
@@ -103,6 +117,19 @@ func (providers *Providers) GetUser(provider string) (string, error) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
		log.Debug().Msg("Got email from google")
 | 
							log.Debug().Msg("Got email from google")
 | 
				
			||||||
		return email, nil
 | 
							return email, nil
 | 
				
			||||||
 | 
						case "tailscale":
 | 
				
			||||||
 | 
							if providers.Tailscale == nil {
 | 
				
			||||||
 | 
								log.Debug().Msg("Tailscale provider not configured")
 | 
				
			||||||
 | 
								return "", nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							client := providers.Tailscale.GetClient()
 | 
				
			||||||
 | 
							log.Debug().Msg("Got client from tailscale")
 | 
				
			||||||
 | 
							email, emailErr := GetTailscaleEmail(client)
 | 
				
			||||||
 | 
							if emailErr != nil {
 | 
				
			||||||
 | 
								return "", emailErr
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							log.Debug().Msg("Got email from tailscale")
 | 
				
			||||||
 | 
							return email, nil
 | 
				
			||||||
	case "generic":
 | 
						case "generic":
 | 
				
			||||||
		if providers.Generic == nil {
 | 
							if providers.Generic == nil {
 | 
				
			||||||
			log.Debug().Msg("Generic provider not configured")
 | 
								log.Debug().Msg("Generic provider not configured")
 | 
				
			||||||
@@ -129,6 +156,9 @@ func (provider *Providers) GetConfiguredProviders() []string {
 | 
				
			|||||||
	if provider.Google != nil {
 | 
						if provider.Google != nil {
 | 
				
			||||||
		providers = append(providers, "google")
 | 
							providers = append(providers, "google")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						if provider.Tailscale != nil {
 | 
				
			||||||
 | 
							providers = append(providers, "tailscale")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	if provider.Generic != nil {
 | 
						if provider.Generic != nil {
 | 
				
			||||||
		providers = append(providers, "generic")
 | 
							providers = append(providers, "generic")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										56
									
								
								internal/providers/tailscale.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								internal/providers/tailscale.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,56 @@
 | 
				
			|||||||
 | 
					package providers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/rs/zerolog/log"
 | 
				
			||||||
 | 
						"golang.org/x/oauth2"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type TailscaleUser struct {
 | 
				
			||||||
 | 
						LoginName string `json:"loginName"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type TailscaleUserInfoResponse struct {
 | 
				
			||||||
 | 
						Users []TailscaleUser `json:"users"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TailscaleScopes() []string {
 | 
				
			||||||
 | 
						return []string{"users:read"}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var TailscaleEndpoint = oauth2.Endpoint{
 | 
				
			||||||
 | 
						TokenURL: "https://api.tailscale.com/api/v2/oauth/token",
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetTailscaleEmail(client *http.Client) (string, error) {
 | 
				
			||||||
 | 
						res, resErr := client.Get("https://api.tailscale.com/api/v2/tailnet/-/users")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if resErr != nil {
 | 
				
			||||||
 | 
							return "", resErr
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						log.Debug().Msg("Got response from tailscale")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						body, bodyErr := io.ReadAll(res.Body)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if bodyErr != nil {
 | 
				
			||||||
 | 
							return "", bodyErr
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						log.Debug().Msg("Read body from tailscale")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var users TailscaleUserInfoResponse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						jsonErr := json.Unmarshal(body, &users)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if jsonErr != nil {
 | 
				
			||||||
 | 
							return "", jsonErr
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						log.Debug().Msg("Parsed users from tailscale")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return users.Users[0].LoginName, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -19,31 +19,34 @@ type User struct {
 | 
				
			|||||||
type Users []User
 | 
					type Users []User
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Config struct {
 | 
					type Config struct {
 | 
				
			||||||
	Port                    int    `validate:"number" mapstructure:"port"`
 | 
						Port                      int    `mapstructure:"port" validate:"required"`
 | 
				
			||||||
	Address                 string `mapstructure:"address, ip4_addr"`
 | 
						Address                   string `validate:"required,ip4_addr" mapstructure:"address"`
 | 
				
			||||||
	Secret                  string `validate:"required,len=32" mapstructure:"secret"`
 | 
						Secret                    string `validate:"required,len=32" mapstructure:"secret"`
 | 
				
			||||||
	SecretFile              string `mapstructure:"secret-file"`
 | 
						SecretFile                string `mapstructure:"secret-file"`
 | 
				
			||||||
	AppURL                  string `validate:"required,url" mapstructure:"app-url"`
 | 
						AppURL                    string `validate:"required,url" mapstructure:"app-url"`
 | 
				
			||||||
	Users                   string `mapstructure:"users"`
 | 
						Users                     string `mapstructure:"users"`
 | 
				
			||||||
	UsersFile               string `mapstructure:"users-file"`
 | 
						UsersFile                 string `mapstructure:"users-file"`
 | 
				
			||||||
	CookieSecure            bool   `mapstructure:"cookie-secure"`
 | 
						CookieSecure              bool   `mapstructure:"cookie-secure"`
 | 
				
			||||||
	GithubClientId          string `mapstructure:"github-client-id"`
 | 
						GithubClientId            string `mapstructure:"github-client-id"`
 | 
				
			||||||
	GithubClientSecret      string `mapstructure:"github-client-secret"`
 | 
						GithubClientSecret        string `mapstructure:"github-client-secret"`
 | 
				
			||||||
	GithubClientSecretFile  string `mapstructure:"github-client-secret-file"`
 | 
						GithubClientSecretFile    string `mapstructure:"github-client-secret-file"`
 | 
				
			||||||
	GoogleClientId          string `mapstructure:"google-client-id"`
 | 
						GoogleClientId            string `mapstructure:"google-client-id"`
 | 
				
			||||||
	GoogleClientSecret      string `mapstructure:"google-client-secret"`
 | 
						GoogleClientSecret        string `mapstructure:"google-client-secret"`
 | 
				
			||||||
	GoogleClientSecretFile  string `mapstructure:"google-client-secret-file"`
 | 
						GoogleClientSecretFile    string `mapstructure:"google-client-secret-file"`
 | 
				
			||||||
	GenericClientId         string `mapstructure:"generic-client-id"`
 | 
						TailscaleClientId         string `mapstructure:"tailscale-client-id"`
 | 
				
			||||||
	GenericClientSecret     string `mapstructure:"generic-client-secret"`
 | 
						TailscaleClientSecret     string `mapstructure:"tailscale-client-secret"`
 | 
				
			||||||
	GenericClientSecretFile string `mapstructure:"generic-client-secret-file"`
 | 
						TailscaleClientSecretFile string `mapstructure:"tailscale-client-secret-file"`
 | 
				
			||||||
	GenericScopes           string `mapstructure:"generic-scopes"`
 | 
						GenericClientId           string `mapstructure:"generic-client-id"`
 | 
				
			||||||
	GenericAuthURL          string `mapstructure:"generic-auth-url"`
 | 
						GenericClientSecret       string `mapstructure:"generic-client-secret"`
 | 
				
			||||||
	GenericTokenURL         string `mapstructure:"generic-token-url"`
 | 
						GenericClientSecretFile   string `mapstructure:"generic-client-secret-file"`
 | 
				
			||||||
	GenericUserURL          string `mapstructure:"generic-user-info-url"`
 | 
						GenericScopes             string `mapstructure:"generic-scopes"`
 | 
				
			||||||
	DisableContinue         bool   `mapstructure:"disable-continue"`
 | 
						GenericAuthURL            string `mapstructure:"generic-auth-url"`
 | 
				
			||||||
	OAuthWhitelist          string `mapstructure:"oauth-whitelist"`
 | 
						GenericTokenURL           string `mapstructure:"generic-token-url"`
 | 
				
			||||||
	CookieExpiry            int    `mapstructure:"cookie-expiry"`
 | 
						GenericUserURL            string `mapstructure:"generic-user-url"`
 | 
				
			||||||
	LogLevel                int8   `mapstructure:"log-level"`
 | 
						DisableContinue           bool   `mapstructure:"disable-continue"`
 | 
				
			||||||
 | 
						OAuthWhitelist            string `mapstructure:"oauth-whitelist"`
 | 
				
			||||||
 | 
						CookieExpiry              int    `mapstructure:"cookie-expiry"`
 | 
				
			||||||
 | 
						LogLevel                  int8   `mapstructure:"log-level" validate:"min=-1,max=5"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type UserContext struct {
 | 
					type UserContext struct {
 | 
				
			||||||
@@ -64,17 +67,19 @@ type APIConfig struct {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type OAuthConfig struct {
 | 
					type OAuthConfig struct {
 | 
				
			||||||
	GithubClientId      string
 | 
						GithubClientId        string
 | 
				
			||||||
	GithubClientSecret  string
 | 
						GithubClientSecret    string
 | 
				
			||||||
	GoogleClientId      string
 | 
						GoogleClientId        string
 | 
				
			||||||
	GoogleClientSecret  string
 | 
						GoogleClientSecret    string
 | 
				
			||||||
	GenericClientId     string
 | 
						TailscaleClientId     string
 | 
				
			||||||
	GenericClientSecret string
 | 
						TailscaleClientSecret string
 | 
				
			||||||
	GenericScopes       []string
 | 
						GenericClientId       string
 | 
				
			||||||
	GenericAuthURL      string
 | 
						GenericClientSecret   string
 | 
				
			||||||
	GenericTokenURL     string
 | 
						GenericScopes         []string
 | 
				
			||||||
	GenericUserURL      string
 | 
						GenericAuthURL        string
 | 
				
			||||||
	AppURL              string
 | 
						GenericTokenURL       string
 | 
				
			||||||
 | 
						GenericUserURL        string
 | 
				
			||||||
 | 
						AppURL                string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type OAuthRequest struct {
 | 
					type OAuthRequest struct {
 | 
				
			||||||
@@ -89,9 +94,19 @@ type OAuthProviders struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
type UnauthorizedQuery struct {
 | 
					type UnauthorizedQuery struct {
 | 
				
			||||||
	Username string `url:"username"`
 | 
						Username string `url:"username"`
 | 
				
			||||||
 | 
						Resource string `url:"resource"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type SessionCookie struct {
 | 
					type SessionCookie struct {
 | 
				
			||||||
	Username string
 | 
						Username string
 | 
				
			||||||
	Provider string
 | 
						Provider string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type TinyauthLabels struct {
 | 
				
			||||||
 | 
						OAuthWhitelist []string
 | 
				
			||||||
 | 
						Users          []string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type TailscaleQuery struct {
 | 
				
			||||||
 | 
						Code int `url:"code"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,9 @@ import (
 | 
				
			|||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
 | 
						"slices"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
						"tinyauth/internal/constants"
 | 
				
			||||||
	"tinyauth/internal/types"
 | 
						"tinyauth/internal/types"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/rs/zerolog/log"
 | 
						"github.com/rs/zerolog/log"
 | 
				
			||||||
@@ -15,15 +17,12 @@ func ParseUsers(users string) (types.Users, error) {
 | 
				
			|||||||
	var usersParsed types.Users
 | 
						var usersParsed types.Users
 | 
				
			||||||
	userList := strings.Split(users, ",")
 | 
						userList := strings.Split(users, ",")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	log.Debug().Strs("users", userList).Msg("Splitted users")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if len(userList) == 0 {
 | 
						if len(userList) == 0 {
 | 
				
			||||||
		return types.Users{}, errors.New("invalid user format")
 | 
							return types.Users{}, errors.New("invalid user format")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, user := range userList {
 | 
						for _, user := range userList {
 | 
				
			||||||
		userSplit := strings.Split(user, ":")
 | 
							userSplit := strings.Split(user, ":")
 | 
				
			||||||
		log.Debug().Strs("user", userSplit).Msg("Splitting user")
 | 
					 | 
				
			||||||
		if len(userSplit) != 2 {
 | 
							if len(userSplit) != 2 {
 | 
				
			||||||
			return types.Users{}, errors.New("invalid user format")
 | 
								return types.Users{}, errors.New("invalid user format")
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -33,7 +32,7 @@ func ParseUsers(users string) (types.Users, error) {
 | 
				
			|||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	log.Debug().Interface("users", usersParsed).Msg("Parsed users")
 | 
						log.Debug().Msg("Parsed users")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return usersParsed, nil
 | 
						return usersParsed, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -45,7 +44,7 @@ func GetRootURL(urlSrc string) (string, error) {
 | 
				
			|||||||
		return "", parseErr
 | 
							return "", parseErr
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	urlSplitted := strings.Split(urlParsed.Host, ".")
 | 
						urlSplitted := strings.Split(urlParsed.Hostname(), ".")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	urlFinal := strings.Join(urlSplitted[1:], ".")
 | 
						urlFinal := strings.Join(urlSplitted[1:], ".")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -77,21 +76,19 @@ func ParseFileToLine(content string) string {
 | 
				
			|||||||
			continue
 | 
								continue
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		users = append(users, line)
 | 
							users = append(users, strings.TrimSpace(line))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return strings.Join(users, ",")
 | 
						return strings.Join(users, ",")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func GetSecret(env string, file string) string {
 | 
					func GetSecret(conf string, file string) string {
 | 
				
			||||||
	if env == "" && file == "" {
 | 
						if conf == "" && file == "" {
 | 
				
			||||||
		log.Debug().Msg("No secret provided")
 | 
					 | 
				
			||||||
		return ""
 | 
							return ""
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if env != "" {
 | 
						if conf != "" {
 | 
				
			||||||
		log.Debug().Str("secret", env).Msg("Using secret from env")
 | 
							return conf
 | 
				
			||||||
		return env
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	contents, err := ReadFile(file)
 | 
						contents, err := ReadFile(file)
 | 
				
			||||||
@@ -100,29 +97,29 @@ func GetSecret(env string, file string) string {
 | 
				
			|||||||
		return ""
 | 
							return ""
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	log.Debug().Str("secret", contents).Msg("Using secret from file")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return contents
 | 
						return contents
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func GetUsers(env string, file string) (types.Users, error) {
 | 
					func GetUsers(conf string, file string) (types.Users, error) {
 | 
				
			||||||
	var users string
 | 
						var users string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if env == "" && file == "" {
 | 
						if conf == "" && file == "" {
 | 
				
			||||||
		return types.Users{}, errors.New("no users provided")
 | 
							return types.Users{}, errors.New("no users provided")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if env != "" {
 | 
						if conf != "" {
 | 
				
			||||||
		log.Debug().Str("users", env).Msg("Using users from env")
 | 
							log.Debug().Msg("Using users from config")
 | 
				
			||||||
		users += env
 | 
							users += conf
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if file != "" {
 | 
						if file != "" {
 | 
				
			||||||
		fileContents, fileErr := ReadFile(file)
 | 
							fileContents, fileErr := ReadFile(file)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if fileErr == nil {
 | 
							if fileErr == nil {
 | 
				
			||||||
			log.Debug().Str("users", ParseFileToLine(fileContents)).Msg("Using users from file")
 | 
								log.Debug().Msg("Using users from file")
 | 
				
			||||||
			users += ","
 | 
								if users != "" {
 | 
				
			||||||
 | 
									users += ","
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
			users += ParseFileToLine(fileContents)
 | 
								users += ParseFileToLine(fileContents)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -133,3 +130,19 @@ func GetUsers(env string, file string) (types.Users, error) {
 | 
				
			|||||||
func OAuthConfigured(config types.Config) bool {
 | 
					func OAuthConfigured(config types.Config) bool {
 | 
				
			||||||
	return (config.GithubClientId != "" && config.GithubClientSecret != "") || (config.GoogleClientId != "" && config.GoogleClientSecret != "") || (config.GenericClientId != "" && config.GenericClientSecret != "")
 | 
						return (config.GithubClientId != "" && config.GithubClientSecret != "") || (config.GoogleClientId != "" && config.GoogleClientSecret != "") || (config.GenericClientId != "" && config.GenericClientSecret != "")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetTinyauthLabels(labels map[string]string) types.TinyauthLabels {
 | 
				
			||||||
 | 
						var tinyauthLabels types.TinyauthLabels
 | 
				
			||||||
 | 
						for label, value := range labels {
 | 
				
			||||||
 | 
							if slices.Contains(constants.TinyauthLabels, label) {
 | 
				
			||||||
 | 
								log.Debug().Str("label", label).Msg("Found label")
 | 
				
			||||||
 | 
								switch label {
 | 
				
			||||||
 | 
								case "tinyauth.oauth.whitelist":
 | 
				
			||||||
 | 
									tinyauthLabels.OAuthWhitelist = strings.Split(value, ",")
 | 
				
			||||||
 | 
								case "tinyauth.users":
 | 
				
			||||||
 | 
									tinyauthLabels.Users = strings.Split(value, ",")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return tinyauthLabels
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										4
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								main.go
									
									
									
									
									
								
							@@ -4,7 +4,6 @@ import (
 | 
				
			|||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
	"tinyauth/cmd"
 | 
						"tinyauth/cmd"
 | 
				
			||||||
	"tinyauth/internal/assets"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/rs/zerolog"
 | 
						"github.com/rs/zerolog"
 | 
				
			||||||
	"github.com/rs/zerolog/log"
 | 
						"github.com/rs/zerolog/log"
 | 
				
			||||||
@@ -12,8 +11,7 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func main() {
 | 
					func main() {
 | 
				
			||||||
	// Logger
 | 
						// Logger
 | 
				
			||||||
	log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Logger()
 | 
						log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Logger().Level(zerolog.FatalLevel)
 | 
				
			||||||
	log.Info().Str("version", assets.Version).Msg("Starting tinyauth")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Run cmd
 | 
						// Run cmd
 | 
				
			||||||
	cmd.Execute()
 | 
						cmd.Execute()
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								site/bun.lockb
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								site/bun.lockb
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										55
									
								
								site/src/icons/tailscale.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								site/src/icons/tailscale.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
				
			|||||||
 | 
					import type { SVGProps } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function TailscaleIcon(props: SVGProps<SVGSVGElement>) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <svg
 | 
				
			||||||
 | 
					      xmlns="http://www.w3.org/2000/svg"
 | 
				
			||||||
 | 
					      viewBox="0 0 512 512"
 | 
				
			||||||
 | 
					      width={24}
 | 
				
			||||||
 | 
					      height={24}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <style>{".st0{opacity:0.2;fill:#CCCAC9;}.st1{fill:#FFFFFF;}"}</style>
 | 
				
			||||||
 | 
					      <g>
 | 
				
			||||||
 | 
					        <g>
 | 
				
			||||||
 | 
					          <path
 | 
				
			||||||
 | 
					            className="st0"
 | 
				
			||||||
 | 
					            d="M65.6,127.7c35.3,0,63.9-28.6,63.9-63.9S100.9,0,65.6,0S1.8,28.6,1.8,63.9S30.4,127.7,65.6,127.7z"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <path
 | 
				
			||||||
 | 
					            className="st1"
 | 
				
			||||||
 | 
					            d="M65.6,318.1c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9S1.8,219,1.8,254.2S30.4,318.1,65.6,318.1z"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <path
 | 
				
			||||||
 | 
					            className="st0"
 | 
				
			||||||
 | 
					            d="M65.6,512c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9S1.8,412.9,1.8,448.1S30.4,512,65.6,512z"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <path
 | 
				
			||||||
 | 
					            className="st1"
 | 
				
			||||||
 | 
					            d="M257.2,318.1c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9s-63.9,28.6-63.9,63.9S221.9,318.1,257.2,318.1z"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <path
 | 
				
			||||||
 | 
					            className="st1"
 | 
				
			||||||
 | 
					            d="M257.2,512c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9s-63.9,28.6-63.9,63.9S221.9,512,257.2,512z"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <path
 | 
				
			||||||
 | 
					            className="st0"
 | 
				
			||||||
 | 
					            d="M257.2,127.7c35.3,0,63.9-28.6,63.9-63.9S292.5,0,257.2,0s-63.9,28.6-63.9,63.9S221.9,127.7,257.2,127.7z"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <path
 | 
				
			||||||
 | 
					            className="st0"
 | 
				
			||||||
 | 
					            d="M446.4,127.7c35.3,0,63.9-28.6,63.9-63.9S481.6,0,446.4,0c-35.3,0-63.9,28.6-63.9,63.9S411.1,127.7,446.4,127.7z"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <path
 | 
				
			||||||
 | 
					            className="st1"
 | 
				
			||||||
 | 
					            d="M446.4,318.1c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9s-63.9,28.6-63.9,63.9S411.1,318.1,446.4,318.1z"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <path
 | 
				
			||||||
 | 
					            className="st0"
 | 
				
			||||||
 | 
					            d="M446.4,512c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9s-63.9,28.6-63.9,63.9S411.1,512,446.4,512z"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					      </g>
 | 
				
			||||||
 | 
					    </svg>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,8 +1,9 @@
 | 
				
			|||||||
import { Button, Paper, Text } from "@mantine/core";
 | 
					import { Button, Code, Paper, Text } from "@mantine/core";
 | 
				
			||||||
import { notifications } from "@mantine/notifications";
 | 
					import { notifications } from "@mantine/notifications";
 | 
				
			||||||
import { Navigate } from "react-router";
 | 
					import { Navigate } from "react-router";
 | 
				
			||||||
import { useUserContext } from "../context/user-context";
 | 
					import { useUserContext } from "../context/user-context";
 | 
				
			||||||
import { Layout } from "../components/layouts/layout";
 | 
					import { Layout } from "../components/layouts/layout";
 | 
				
			||||||
 | 
					import { ReactNode } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ContinuePage = () => {
 | 
					export const ContinuePage = () => {
 | 
				
			||||||
  const queryString = window.location.search;
 | 
					  const queryString = window.location.search;
 | 
				
			||||||
@@ -12,11 +13,11 @@ export const ContinuePage = () => {
 | 
				
			|||||||
  const { isLoggedIn, disableContinue } = useUserContext();
 | 
					  const { isLoggedIn, disableContinue } = useUserContext();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!isLoggedIn) {
 | 
					  if (!isLoggedIn) {
 | 
				
			||||||
    return <Navigate to="/login" />;
 | 
					    return <Navigate to={`/login?redirect_uri=${redirectUri}`} />;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (disableContinue && redirectUri !== "null") {
 | 
					  if (redirectUri === "null") {
 | 
				
			||||||
    window.location.replace(redirectUri!);
 | 
					    return <Navigate to="/" />;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const redirect = () => {
 | 
					  const redirect = () => {
 | 
				
			||||||
@@ -26,31 +27,62 @@ export const ContinuePage = () => {
 | 
				
			|||||||
      color: "blue",
 | 
					      color: "blue",
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    setTimeout(() => {
 | 
					    setTimeout(() => {
 | 
				
			||||||
      window.location.replace(redirectUri!);
 | 
					      window.location.href = redirectUri!;
 | 
				
			||||||
    }, 500);
 | 
					    }, 500);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const urlParsed = URL.parse(redirectUri!);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (
 | 
				
			||||||
 | 
					    window.location.protocol === "https:" &&
 | 
				
			||||||
 | 
					    urlParsed!.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) {
 | 
				
			||||||
 | 
					    window.location.href = redirectUri!;
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <ContinuePageLayout>
 | 
				
			||||||
 | 
					        <Text size="xl" fw={700}>
 | 
				
			||||||
 | 
					          Redirecting
 | 
				
			||||||
 | 
					        </Text>
 | 
				
			||||||
 | 
					        <Text>You should be redirected to your app soon.</Text>
 | 
				
			||||||
 | 
					      </ContinuePageLayout>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <ContinuePageLayout>
 | 
				
			||||||
 | 
					      <Text size="xl" fw={700}>
 | 
				
			||||||
 | 
					        Continue
 | 
				
			||||||
 | 
					      </Text>
 | 
				
			||||||
 | 
					      <Text>Click the button to continue to your app.</Text>
 | 
				
			||||||
 | 
					      <Button fullWidth mt="xl" onClick={redirect}>
 | 
				
			||||||
 | 
					        Continue
 | 
				
			||||||
 | 
					      </Button>
 | 
				
			||||||
 | 
					    </ContinuePageLayout>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ContinuePageLayout = ({ children }: { children: ReactNode }) => {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Layout>
 | 
					    <Layout>
 | 
				
			||||||
      <Paper shadow="md" p={30} mt={30} radius="md" withBorder>
 | 
					      <Paper shadow="md" p={30} mt={30} radius="md" withBorder>
 | 
				
			||||||
        {redirectUri !== "null" ? (
 | 
					        {children}
 | 
				
			||||||
          <>
 | 
					 | 
				
			||||||
            <Text size="xl" fw={700}>
 | 
					 | 
				
			||||||
              Continue
 | 
					 | 
				
			||||||
            </Text>
 | 
					 | 
				
			||||||
            <Text>Click the button to continue to your app.</Text>
 | 
					 | 
				
			||||||
            <Button fullWidth mt="xl" onClick={redirect}>
 | 
					 | 
				
			||||||
              Continue
 | 
					 | 
				
			||||||
            </Button>
 | 
					 | 
				
			||||||
          </>
 | 
					 | 
				
			||||||
        ) : (
 | 
					 | 
				
			||||||
          <>
 | 
					 | 
				
			||||||
            <Text size="xl" fw={700}>
 | 
					 | 
				
			||||||
              Logged in
 | 
					 | 
				
			||||||
            </Text>
 | 
					 | 
				
			||||||
            <Text>You are now signed in and can use your apps.</Text>
 | 
					 | 
				
			||||||
          </>
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
      </Paper>
 | 
					      </Paper>
 | 
				
			||||||
    </Layout>
 | 
					    </Layout>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,6 +19,7 @@ import { Layout } from "../components/layouts/layout";
 | 
				
			|||||||
import { GoogleIcon } from "../icons/google";
 | 
					import { GoogleIcon } from "../icons/google";
 | 
				
			||||||
import { GithubIcon } from "../icons/github";
 | 
					import { GithubIcon } from "../icons/github";
 | 
				
			||||||
import { OAuthIcon } from "../icons/oauth";
 | 
					import { OAuthIcon } from "../icons/oauth";
 | 
				
			||||||
 | 
					import { TailscaleIcon } from "../icons/tailscale";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const LoginPage = () => {
 | 
					export const LoginPage = () => {
 | 
				
			||||||
  const queryString = window.location.search;
 | 
					  const queryString = window.location.search;
 | 
				
			||||||
@@ -26,6 +27,9 @@ export const LoginPage = () => {
 | 
				
			|||||||
  const redirectUri = params.get("redirect_uri");
 | 
					  const redirectUri = params.get("redirect_uri");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { isLoggedIn, configuredProviders } = useUserContext();
 | 
					  const { isLoggedIn, configuredProviders } = useUserContext();
 | 
				
			||||||
 | 
					  const oauthProviders = configuredProviders.filter(
 | 
				
			||||||
 | 
					    (value) => value !== "username",
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (isLoggedIn) {
 | 
					  if (isLoggedIn) {
 | 
				
			||||||
    return <Navigate to="/logout" />;
 | 
					    return <Navigate to="/logout" />;
 | 
				
			||||||
@@ -65,8 +69,12 @@ export const LoginPage = () => {
 | 
				
			|||||||
        color: "green",
 | 
					        color: "green",
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      setTimeout(() => {
 | 
					      setTimeout(() => {
 | 
				
			||||||
        window.location.replace(`/continue?redirect_uri=${redirectUri}`);
 | 
					        if (redirectUri === "null") {
 | 
				
			||||||
      });
 | 
					          window.location.replace("/");
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          window.location.replace(`/continue?redirect_uri=${redirectUri}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }, 500);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -84,7 +92,14 @@ export const LoginPage = () => {
 | 
				
			|||||||
      });
 | 
					      });
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    onSuccess: (data) => {
 | 
					    onSuccess: (data) => {
 | 
				
			||||||
      window.location.replace(data.data.url);
 | 
					      notifications.show({
 | 
				
			||||||
 | 
					        title: "Redirecting",
 | 
				
			||||||
 | 
					        message: "Redirecting to your OAuth provider",
 | 
				
			||||||
 | 
					        color: "blue",
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      setTimeout(() => {
 | 
				
			||||||
 | 
					        window.location.href = data.data.url;
 | 
				
			||||||
 | 
					      }, 500);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -96,18 +111,13 @@ export const LoginPage = () => {
 | 
				
			|||||||
    <Layout>
 | 
					    <Layout>
 | 
				
			||||||
      <Title ta="center">Tinyauth</Title>
 | 
					      <Title ta="center">Tinyauth</Title>
 | 
				
			||||||
      <Paper shadow="md" p="xl" mt={30} radius="md" withBorder>
 | 
					      <Paper shadow="md" p="xl" mt={30} radius="md" withBorder>
 | 
				
			||||||
        {configuredProviders.length === 0 && (
 | 
					        {oauthProviders.length > 0 && (
 | 
				
			||||||
          <Text size="lg" mb="md" fw={500} ta="center">
 | 
					 | 
				
			||||||
            Welcome back, please login
 | 
					 | 
				
			||||||
          </Text>
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
        {configuredProviders.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">
 | 
					            <Grid mb="md" mt="md" align="center" justify="center">
 | 
				
			||||||
              {configuredProviders.includes("google") && (
 | 
					              {oauthProviders.includes("google") && (
 | 
				
			||||||
                <Grid.Col span="content">
 | 
					                <Grid.Col span="content">
 | 
				
			||||||
                  <Button
 | 
					                  <Button
 | 
				
			||||||
                    radius="xl"
 | 
					                    radius="xl"
 | 
				
			||||||
@@ -122,7 +132,7 @@ export const LoginPage = () => {
 | 
				
			|||||||
                  </Button>
 | 
					                  </Button>
 | 
				
			||||||
                </Grid.Col>
 | 
					                </Grid.Col>
 | 
				
			||||||
              )}
 | 
					              )}
 | 
				
			||||||
              {configuredProviders.includes("github") && (
 | 
					              {oauthProviders.includes("github") && (
 | 
				
			||||||
                <Grid.Col span="content">
 | 
					                <Grid.Col span="content">
 | 
				
			||||||
                  <Button
 | 
					                  <Button
 | 
				
			||||||
                    radius="xl"
 | 
					                    radius="xl"
 | 
				
			||||||
@@ -137,7 +147,22 @@ export const LoginPage = () => {
 | 
				
			|||||||
                  </Button>
 | 
					                  </Button>
 | 
				
			||||||
                </Grid.Col>
 | 
					                </Grid.Col>
 | 
				
			||||||
              )}
 | 
					              )}
 | 
				
			||||||
              {configuredProviders.includes("generic") && (
 | 
					              {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">
 | 
					                <Grid.Col span="content">
 | 
				
			||||||
                  <Button
 | 
					                  <Button
 | 
				
			||||||
                    radius="xl"
 | 
					                    radius="xl"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,7 +32,7 @@ export const LogoutPage = () => {
 | 
				
			|||||||
        color: "green",
 | 
					        color: "green",
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      setTimeout(() => {
 | 
					      setTimeout(() => {
 | 
				
			||||||
        window.location.reload();
 | 
					        window.location.replace("/login");
 | 
				
			||||||
      }, 500);
 | 
					      }, 500);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
@@ -45,8 +45,8 @@ 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)}`}. Click the button below to
 | 
					          {oauth && ` using ${capitalize(provider)} OAuth`}. Click the button
 | 
				
			||||||
          log out.
 | 
					          below to log out.
 | 
				
			||||||
        </Text>
 | 
					        </Text>
 | 
				
			||||||
        <Button
 | 
					        <Button
 | 
				
			||||||
          fullWidth
 | 
					          fullWidth
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,18 +1,12 @@
 | 
				
			|||||||
import { Button, Code, Paper, Text } from "@mantine/core";
 | 
					import { Button, Code, Paper, Text } from "@mantine/core";
 | 
				
			||||||
import { Layout } from "../components/layouts/layout";
 | 
					import { Layout } from "../components/layouts/layout";
 | 
				
			||||||
import { useUserContext } from "../context/user-context";
 | 
					 | 
				
			||||||
import { Navigate } from "react-router";
 | 
					import { Navigate } from "react-router";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const UnauthorizedPage = () => {
 | 
					export const UnauthorizedPage = () => {
 | 
				
			||||||
  const queryString = window.location.search;
 | 
					  const queryString = window.location.search;
 | 
				
			||||||
  const params = new URLSearchParams(queryString);
 | 
					  const params = new URLSearchParams(queryString);
 | 
				
			||||||
  const username = params.get("username");
 | 
					  const username = params.get("username");
 | 
				
			||||||
 | 
					  const resource = params.get("resource");
 | 
				
			||||||
  const { isLoggedIn } = useUserContext();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (isLoggedIn) {
 | 
					 | 
				
			||||||
    return <Navigate to="/" />;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (username === "null") {
 | 
					  if (username === "null") {
 | 
				
			||||||
    return <Navigate to="/" />;
 | 
					    return <Navigate to="/" />;
 | 
				
			||||||
@@ -25,8 +19,14 @@ export const UnauthorizedPage = () => {
 | 
				
			|||||||
          Unauthorized
 | 
					          Unauthorized
 | 
				
			||||||
        </Text>
 | 
					        </Text>
 | 
				
			||||||
        <Text>
 | 
					        <Text>
 | 
				
			||||||
          The user with username <Code>{username}</Code> is not authorized to
 | 
					          The user with username <Code>{username}</Code> is not authorized to{" "}
 | 
				
			||||||
          login.
 | 
					          {resource !== "null" ? (
 | 
				
			||||||
 | 
					            <span>
 | 
				
			||||||
 | 
					              access the <Code>{resource}</Code> resource.
 | 
				
			||||||
 | 
					            </span>
 | 
				
			||||||
 | 
					          ) : (
 | 
				
			||||||
 | 
					            "login."
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
        </Text>
 | 
					        </Text>
 | 
				
			||||||
        <Button
 | 
					        <Button
 | 
				
			||||||
          fullWidth
 | 
					          fullWidth
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user