From 504a3b87b4dbf8bd5529e68f9040d7a72fce611a Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 26 Aug 2025 15:05:03 +0300 Subject: [PATCH 001/127] refactor: rework file structure (#325) * wip: add middlewares * refactor: use context fom middleware in handlers * refactor: use controller approach in handlers * refactor: move oauth providers into services (non-working) * feat: create oauth broker service * refactor: use a boostrap service to bootstrap the app * refactor: split utils into smaller files * refactor: use more clear name for frontend assets * feat: allow customizability of resources dir * fix: fix typo in ui middleware * fix: validate resource file paths in ui middleware * refactor: move resource handling to a controller * feat: add some logging * fix: configure middlewares before groups * fix: use correct api path in login mutation * fix: coderabbit suggestions * fix: further coderabbit suggestions --- .env.example | 4 +- .gitignore | 5 +- air.toml | 2 +- cmd/root.go | 297 +++------- cmd/version.go | 8 +- frontend/src/context/app-context.tsx | 2 +- frontend/src/context/user-context.tsx | 2 +- frontend/src/pages/login-page.tsx | 2 +- frontend/src/pages/logout-page.tsx | 2 +- frontend/src/pages/totp-page.tsx | 2 +- frontend/vite.config.ts | 5 + go.mod | 1 + go.sum | 2 + internal/assets/assets.go | 4 +- internal/auth/auth_test.go | 146 ----- internal/bootstrap/app_bootstrap.go | 260 +++++++++ internal/{types => config}/config.go | 149 +++-- internal/constants/constants.go | 19 - internal/controller/context_controller.go | 104 ++++ internal/controller/health_controller.go | 25 + internal/controller/oauth_controller.go | 200 +++++++ internal/controller/proxy_controller.go | 311 ++++++++++ internal/controller/resources_controller.go | 42 ++ internal/controller/user_controller.go | 266 +++++++++ internal/handlers/context.go | 64 -- internal/handlers/handlers.go | 36 -- internal/handlers/handlers_test.go | 394 ------------- internal/handlers/oauth.go | 223 ------- internal/handlers/proxy.go | 282 --------- internal/handlers/user.go | 197 ------- internal/hooks/hooks.go | 144 ----- internal/middleware/context_middleware.go | 159 +++++ internal/middleware/ui_middleware.go | 56 ++ internal/middleware/zerolog_middleware.go | 66 +++ internal/oauth/oauth.go | 71 --- internal/providers/generic.go | 37 -- internal/providers/github.go | 102 ---- internal/providers/google.go | 56 -- internal/providers/providers.go | 154 ----- internal/server/server.go | 130 ----- .../{auth/auth.go => service/auth_service.go} | 189 +++--- .../docker.go => service/docker_service.go} | 50 +- internal/service/generic_oauth_service.go | 117 ++++ internal/service/github_oauth_service.go | 169 ++++++ internal/service/google_oauth_service.go | 113 ++++ .../{ldap/ldap.go => service/ldap_service.go} | 64 +- internal/service/oauth_broker_service.go | 76 +++ internal/types/api.go | 62 -- internal/types/types.go | 59 -- internal/utils/app_utils.go | 123 ++++ internal/utils/fs_utils.go | 17 + internal/utils/label_utils.go | 48 ++ internal/utils/security_utils.go | 124 ++++ internal/utils/string_utils.go | 30 + internal/utils/user_utils.go | 92 +++ internal/utils/utils.go | 350 ----------- internal/utils/utils_test.go | 548 ------------------ main.go | 2 +- 58 files changed, 2731 insertions(+), 3533 deletions(-) delete mode 100644 internal/auth/auth_test.go create mode 100644 internal/bootstrap/app_bootstrap.go rename internal/{types => config}/config.go (56%) delete mode 100644 internal/constants/constants.go create mode 100644 internal/controller/context_controller.go create mode 100644 internal/controller/health_controller.go create mode 100644 internal/controller/oauth_controller.go create mode 100644 internal/controller/proxy_controller.go create mode 100644 internal/controller/resources_controller.go create mode 100644 internal/controller/user_controller.go delete mode 100644 internal/handlers/context.go delete mode 100644 internal/handlers/handlers.go delete mode 100644 internal/handlers/handlers_test.go delete mode 100644 internal/handlers/oauth.go delete mode 100644 internal/handlers/proxy.go delete mode 100644 internal/handlers/user.go delete mode 100644 internal/hooks/hooks.go create mode 100644 internal/middleware/context_middleware.go create mode 100644 internal/middleware/ui_middleware.go create mode 100644 internal/middleware/zerolog_middleware.go delete mode 100644 internal/oauth/oauth.go delete mode 100644 internal/providers/generic.go delete mode 100644 internal/providers/github.go delete mode 100644 internal/providers/google.go delete mode 100644 internal/providers/providers.go delete mode 100644 internal/server/server.go rename internal/{auth/auth.go => service/auth_service.go} (62%) rename internal/{docker/docker.go => service/docker_service.go} (64%) create mode 100644 internal/service/generic_oauth_service.go create mode 100644 internal/service/github_oauth_service.go create mode 100644 internal/service/google_oauth_service.go rename internal/{ldap/ldap.go => service/ldap_service.go} (61%) create mode 100644 internal/service/oauth_broker_service.go delete mode 100644 internal/types/api.go delete mode 100644 internal/types/types.go create mode 100644 internal/utils/app_utils.go create mode 100644 internal/utils/fs_utils.go create mode 100644 internal/utils/label_utils.go create mode 100644 internal/utils/security_utils.go create mode 100644 internal/utils/string_utils.go create mode 100644 internal/utils/user_utils.go delete mode 100644 internal/utils/utils.go delete mode 100644 internal/utils/utils_test.go diff --git a/.env.example b/.env.example index 8edde7b7..0f43bf04 100644 --- a/.env.example +++ b/.env.example @@ -5,7 +5,7 @@ SECRET_FILE=app_secret_file APP_URL=http://localhost:3000 USERS=your_user_password_hash USERS_FILE=users_file -COOKIE_SECURE=false +SECURE_COOKIE=false GITHUB_CLIENT_ID=github_client_id GITHUB_CLIENT_SECRET=github_client_secret GITHUB_CLIENT_SECRET_FILE=github_client_secret_file @@ -25,7 +25,7 @@ GENERIC_NAME=My OAuth SESSION_EXPIRY=7200 LOGIN_TIMEOUT=300 LOGIN_MAX_RETRIES=5 -LOG_LEVEL=0 +LOG_LEVEL=debug APP_TITLE=Tinyauth SSO FORGOT_PASSWORD_MESSAGE=Some message about resetting the password OAUTH_AUTO_REDIRECT=none diff --git a/.gitignore b/.gitignore index 0100a134..cb79b93b 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,7 @@ secret* tmp # version files -internal/assets/version \ No newline at end of file +internal/assets/version + +# data directory +data \ No newline at end of file diff --git a/air.toml b/air.toml index 7505b79a..f84163bc 100644 --- a/air.toml +++ b/air.toml @@ -4,7 +4,7 @@ tmp_dir = "tmp" [build] pre_cmd = ["mkdir -p internal/assets/dist", "echo 'backend running' > internal/assets/dist/index.html", "go install github.com/go-delve/delve/cmd/dlv@v1.25.0"] cmd = "CGO_ENABLED=0 go build -gcflags=\"all=-N -l\" -o tmp/tinyauth ." -bin = "/go/bin/dlv --listen :4000 --headless=true --api-version=2 --accept-multiclient --log=true exec tmp/tinyauth --continue" +bin = "/go/bin/dlv --listen :4000 --headless=true --api-version=2 --accept-multiclient --log=true exec tmp/tinyauth --continue --check-go-version=false" include_ext = ["go"] exclude_dir = ["internal/assets/dist"] exclude_regex = [".*_test\\.go"] diff --git a/cmd/root.go b/cmd/root.go index f96ec6bc..ef5733e1 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,20 +1,11 @@ package cmd import ( - "errors" - "fmt" "strings" totpCmd "tinyauth/cmd/totp" userCmd "tinyauth/cmd/user" - "tinyauth/internal/auth" - "tinyauth/internal/constants" - "tinyauth/internal/docker" - "tinyauth/internal/handlers" - "tinyauth/internal/hooks" - "tinyauth/internal/ldap" - "tinyauth/internal/providers" - "tinyauth/internal/server" - "tinyauth/internal/types" + "tinyauth/internal/bootstrap" + "tinyauth/internal/config" "tinyauth/internal/utils" "github.com/go-playground/validator/v10" @@ -29,147 +20,47 @@ var rootCmd = &cobra.Command{ Short: "The simplest way to protect your apps with a login screen.", Long: `Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic OAuth provider to all of your docker apps.`, Run: func(cmd *cobra.Command, args []string) { - var config types.Config - err := viper.Unmarshal(&config) - HandleError(err, "Failed to parse config") + var conf config.Config - // Check if secrets have a file associated with them - 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) - - validator := validator.New() - err = validator.Struct(config) - HandleError(err, "Failed to validate config") - - log.Logger = log.Level(zerolog.Level(config.LogLevel)) - log.Info().Str("version", strings.TrimSpace(constants.Version)).Msg("Starting tinyauth") - - log.Info().Msg("Parsing users") - users, err := utils.GetUsers(config.Users, config.UsersFile) - HandleError(err, "Failed to parse users") - - log.Debug().Msg("Getting domain") - domain, err := utils.GetUpperDomain(config.AppURL) - HandleError(err, "Failed to get upper domain") - log.Info().Str("domain", domain).Msg("Using domain for cookie store") - - cookieId := utils.GenerateIdentifier(strings.Split(domain, ".")[0]) - sessionCookieName := fmt.Sprintf("%s-%s", constants.SessionCookieName, cookieId) - csrfCookieName := fmt.Sprintf("%s-%s", constants.CsrfCookieName, cookieId) - redirectCookieName := fmt.Sprintf("%s-%s", constants.RedirectCookieName, cookieId) - - log.Debug().Msg("Deriving HMAC and encryption secrets") - - hmacSecret, err := utils.DeriveKey(config.Secret, "hmac") - HandleError(err, "Failed to derive HMAC secret") - - encryptionSecret, err := utils.DeriveKey(config.Secret, "encryption") - HandleError(err, "Failed to derive encryption secret") - - // Split the config into service-specific sub-configs - oauthConfig := types.OAuthConfig{ - GithubClientId: config.GithubClientId, - GithubClientSecret: config.GithubClientSecret, - GoogleClientId: config.GoogleClientId, - GoogleClientSecret: config.GoogleClientSecret, - GenericClientId: config.GenericClientId, - GenericClientSecret: config.GenericClientSecret, - GenericScopes: strings.Split(config.GenericScopes, ","), - GenericAuthURL: config.GenericAuthURL, - GenericTokenURL: config.GenericTokenURL, - GenericUserURL: config.GenericUserURL, - GenericSkipSSL: config.GenericSkipSSL, - AppURL: config.AppURL, + err := viper.Unmarshal(&conf) + if err != nil { + log.Fatal().Err(err).Msg("Failed to parse config") } - handlersConfig := types.HandlersConfig{ - AppURL: config.AppURL, - DisableContinue: config.DisableContinue, - Title: config.Title, - GenericName: config.GenericName, - CookieSecure: config.CookieSecure, - Domain: domain, - ForgotPasswordMessage: config.FogotPasswordMessage, - BackgroundImage: config.BackgroundImage, - OAuthAutoRedirect: config.OAuthAutoRedirect, - CsrfCookieName: csrfCookieName, - RedirectCookieName: redirectCookieName, - } + // Check if secrets have a file associated with them + conf.Secret = utils.GetSecret(conf.Secret, conf.SecretFile) + conf.GithubClientSecret = utils.GetSecret(conf.GithubClientSecret, conf.GithubClientSecretFile) + conf.GoogleClientSecret = utils.GetSecret(conf.GoogleClientSecret, conf.GoogleClientSecretFile) + conf.GenericClientSecret = utils.GetSecret(conf.GenericClientSecret, conf.GenericClientSecretFile) - serverConfig := types.ServerConfig{ - Port: config.Port, - Address: config.Address, - } + // Validate config + v := validator.New() - authConfig := types.AuthConfig{ - Users: users, - OauthWhitelist: config.OAuthWhitelist, - CookieSecure: config.CookieSecure, - SessionExpiry: config.SessionExpiry, - Domain: domain, - LoginTimeout: config.LoginTimeout, - LoginMaxRetries: config.LoginMaxRetries, - SessionCookieName: sessionCookieName, - HMACSecret: hmacSecret, - EncryptionSecret: encryptionSecret, + err = v.Struct(conf) + if err != nil { + log.Fatal().Err(err).Msg("Invalid config") } - hooksConfig := types.HooksConfig{ - Domain: domain, - } + log.Logger = log.Level(zerolog.Level(utils.GetLogLevel(conf.LogLevel))) + log.Info().Str("version", strings.TrimSpace(config.Version)).Msg("Starting tinyauth") - var ldapService *ldap.LDAP + // Create bootstrap app + app := bootstrap.NewBootstrapApp(conf) - if config.LdapAddress != "" { - log.Info().Msg("Using LDAP for authentication") - ldapConfig := types.LdapConfig{ - Address: config.LdapAddress, - BindDN: config.LdapBindDN, - BindPassword: config.LdapBindPassword, - BaseDN: config.LdapBaseDN, - Insecure: config.LdapInsecure, - SearchFilter: config.LdapSearchFilter, - } - ldapService, err = ldap.NewLDAP(ldapConfig) - if err != nil { - log.Error().Err(err).Msg("Failed to initialize LDAP service, disabling LDAP authentication") - ldapService = nil - } - } else { - log.Info().Msg("LDAP not configured, using local users or OAuth") - } + // Run + err = app.Setup() - // Check if we have a source of users - if len(users) == 0 && !utils.OAuthConfigured(config) && ldapService == nil { - HandleError(errors.New("err no users"), "Unable to find a source of users") + if err != nil { + log.Fatal().Err(err).Msg("Failed to setup app") } - // Setup the services - docker, err := docker.NewDocker() - HandleError(err, "Failed to initialize docker") - auth := auth.NewAuth(authConfig, docker, ldapService) - providers := providers.NewProviders(oauthConfig) - hooks := hooks.NewHooks(hooksConfig, auth, providers) - handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker) - srv, err := server.NewServer(serverConfig, handlers) - HandleError(err, "Failed to create server") - - // Start up - err = srv.Start() - HandleError(err, "Failed to start server") }, } func Execute() { err := rootCmd.Execute() - HandleError(err, "Failed to execute root command") -} - -func HandleError(err error, msg string) { if err != nil { - log.Fatal().Err(err).Msg(msg) + log.Fatal().Err(err).Msg("Failed to execute command") } } @@ -179,85 +70,67 @@ func init() { viper.AutomaticEnv() - rootCmd.Flags().Int("port", 3000, "Port to run the server on.") - rootCmd.Flags().String("address", "0.0.0.0", "Address to bind the server to.") - rootCmd.Flags().String("secret", "", "Secret to use for the cookie.") - rootCmd.Flags().String("secret-file", "", "Path to a file containing the secret.") - rootCmd.Flags().String("app-url", "", "The tinyauth URL.") - rootCmd.Flags().String("users", "", "Comma separated list of users in the format username:hash.") - rootCmd.Flags().String("users-file", "", "Path to a file containing users in the format username:hash.") - rootCmd.Flags().Bool("cookie-secure", false, "Send cookie over secure connection only.") - rootCmd.Flags().String("github-client-id", "", "Github OAuth client ID.") - rootCmd.Flags().String("github-client-secret", "", "Github OAuth client secret.") - rootCmd.Flags().String("github-client-secret-file", "", "Github OAuth client secret file.") - 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-file", "", "Google OAuth client secret file.") - 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-file", "", "Generic OAuth client secret file.") - rootCmd.Flags().String("generic-scopes", "", "Generic OAuth scopes.") - rootCmd.Flags().String("generic-auth-url", "", "Generic OAuth auth URL.") - rootCmd.Flags().String("generic-token-url", "", "Generic OAuth token URL.") - rootCmd.Flags().String("generic-user-url", "", "Generic OAuth user info URL.") - rootCmd.Flags().String("generic-name", "Generic", "Generic OAuth provider name.") - rootCmd.Flags().Bool("generic-skip-ssl", false, "Skip SSL verification for the generic OAuth provider.") - rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.") - rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.") - rootCmd.Flags().String("oauth-auto-redirect", "none", "Auto redirect to the specified OAuth provider if configured. (available providers: github, google, generic)") - rootCmd.Flags().Int("session-expiry", 86400, "Session (cookie) expiration time in seconds.") - rootCmd.Flags().Int("login-timeout", 300, "Login timeout in seconds after max retries reached (0 to disable).") - rootCmd.Flags().Int("login-max-retries", 5, "Maximum login attempts before timeout (0 to disable).") - rootCmd.Flags().Int("log-level", 1, "Log level.") - rootCmd.Flags().String("app-title", "Tinyauth", "Title of the app.") - rootCmd.Flags().String("forgot-password-message", "", "Message to show on the forgot password page.") - rootCmd.Flags().String("background-image", "/background.jpg", "Background image URL for the login page.") - rootCmd.Flags().String("ldap-address", "", "LDAP server address (e.g. ldap://localhost:389).") - rootCmd.Flags().String("ldap-bind-dn", "", "LDAP bind DN (e.g. uid=user,dc=example,dc=com).") - rootCmd.Flags().String("ldap-bind-password", "", "LDAP bind password.") - rootCmd.Flags().String("ldap-base-dn", "", "LDAP base DN (e.g. dc=example,dc=com).") - rootCmd.Flags().Bool("ldap-insecure", false, "Skip certificate verification for the LDAP server.") - rootCmd.Flags().String("ldap-search-filter", "(uid=%s)", "LDAP search filter for user lookup.") + configOptions := []struct { + name string + defaultVal any + description string + }{ + {"port", 3000, "Port to run the server on."}, + {"address", "0.0.0.0", "Address to bind the server to."}, + {"secret", "", "Secret to use for the cookie."}, + {"secret-file", "", "Path to a file containing the secret."}, + {"app-url", "", "The Tinyauth URL."}, + {"users", "", "Comma separated list of users in the format username:hash."}, + {"users-file", "", "Path to a file containing users in the format username:hash."}, + {"secure-cookie", false, "Send cookie over secure connection only."}, + {"github-client-id", "", "Github OAuth client ID."}, + {"github-client-secret", "", "Github OAuth client secret."}, + {"github-client-secret-file", "", "Github OAuth client secret file."}, + {"google-client-id", "", "Google OAuth client ID."}, + {"google-client-secret", "", "Google OAuth client secret."}, + {"google-client-secret-file", "", "Google OAuth client secret file."}, + {"generic-client-id", "", "Generic OAuth client ID."}, + {"generic-client-secret", "", "Generic OAuth client secret."}, + {"generic-client-secret-file", "", "Generic OAuth client secret file."}, + {"generic-scopes", "", "Generic OAuth scopes."}, + {"generic-auth-url", "", "Generic OAuth auth URL."}, + {"generic-token-url", "", "Generic OAuth token URL."}, + {"generic-user-url", "", "Generic OAuth user info URL."}, + {"generic-name", "Generic", "Generic OAuth provider name."}, + {"generic-skip-ssl", false, "Skip SSL verification for the generic OAuth provider."}, + {"disable-continue", false, "Disable continue screen and redirect to app directly."}, + {"oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth."}, + {"oauth-auto-redirect", "none", "Auto redirect to the specified OAuth provider if configured. (available providers: github, google, generic)"}, + {"session-expiry", 86400, "Session (cookie) expiration time in seconds."}, + {"login-timeout", 300, "Login timeout in seconds after max retries reached (0 to disable)."}, + {"login-max-retries", 5, "Maximum login attempts before timeout (0 to disable)."}, + {"log-level", "info", "Log level."}, + {"app-title", "Tinyauth", "Title of the app."}, + {"forgot-password-message", "", "Message to show on the forgot password page."}, + {"background-image", "/background.jpg", "Background image URL for the login page."}, + {"ldap-address", "", "LDAP server address (e.g. ldap://localhost:389)."}, + {"ldap-bind-dn", "", "LDAP bind DN (e.g. uid=user,dc=example,dc=com)."}, + {"ldap-bind-password", "", "LDAP bind password."}, + {"ldap-base-dn", "", "LDAP base DN (e.g. dc=example,dc=com)."}, + {"ldap-insecure", false, "Skip certificate verification for the LDAP server."}, + {"ldap-search-filter", "(uid=%s)", "LDAP search filter for user lookup."}, + {"resources-dir", "/data/resources", "Path to a directory containing custom resources (e.g. background image)."}, + } - viper.BindEnv("port", "PORT") - viper.BindEnv("address", "ADDRESS") - viper.BindEnv("secret", "SECRET") - viper.BindEnv("secret-file", "SECRET_FILE") - viper.BindEnv("app-url", "APP_URL") - viper.BindEnv("users", "USERS") - viper.BindEnv("users-file", "USERS_FILE") - viper.BindEnv("cookie-secure", "COOKIE_SECURE") - viper.BindEnv("github-client-id", "GITHUB_CLIENT_ID") - viper.BindEnv("github-client-secret", "GITHUB_CLIENT_SECRET") - viper.BindEnv("github-client-secret-file", "GITHUB_CLIENT_SECRET_FILE") - viper.BindEnv("google-client-id", "GOOGLE_CLIENT_ID") - viper.BindEnv("google-client-secret", "GOOGLE_CLIENT_SECRET") - viper.BindEnv("google-client-secret-file", "GOOGLE_CLIENT_SECRET_FILE") - viper.BindEnv("generic-client-id", "GENERIC_CLIENT_ID") - viper.BindEnv("generic-client-secret", "GENERIC_CLIENT_SECRET") - viper.BindEnv("generic-client-secret-file", "GENERIC_CLIENT_SECRET_FILE") - viper.BindEnv("generic-scopes", "GENERIC_SCOPES") - viper.BindEnv("generic-auth-url", "GENERIC_AUTH_URL") - viper.BindEnv("generic-token-url", "GENERIC_TOKEN_URL") - viper.BindEnv("generic-user-url", "GENERIC_USER_URL") - viper.BindEnv("generic-name", "GENERIC_NAME") - viper.BindEnv("generic-skip-ssl", "GENERIC_SKIP_SSL") - viper.BindEnv("disable-continue", "DISABLE_CONTINUE") - viper.BindEnv("oauth-whitelist", "OAUTH_WHITELIST") - viper.BindEnv("oauth-auto-redirect", "OAUTH_AUTO_REDIRECT") - viper.BindEnv("session-expiry", "SESSION_EXPIRY") - viper.BindEnv("log-level", "LOG_LEVEL") - viper.BindEnv("app-title", "APP_TITLE") - viper.BindEnv("login-timeout", "LOGIN_TIMEOUT") - viper.BindEnv("login-max-retries", "LOGIN_MAX_RETRIES") - viper.BindEnv("forgot-password-message", "FORGOT_PASSWORD_MESSAGE") - viper.BindEnv("background-image", "BACKGROUND_IMAGE") - viper.BindEnv("ldap-address", "LDAP_ADDRESS") - viper.BindEnv("ldap-bind-dn", "LDAP_BIND_DN") - viper.BindEnv("ldap-bind-password", "LDAP_BIND_PASSWORD") - viper.BindEnv("ldap-base-dn", "LDAP_BASE_DN") - viper.BindEnv("ldap-insecure", "LDAP_INSECURE") - viper.BindEnv("ldap-search-filter", "LDAP_SEARCH_FILTER") + for _, opt := range configOptions { + switch v := opt.defaultVal.(type) { + case bool: + rootCmd.Flags().Bool(opt.name, v, opt.description) + case int: + rootCmd.Flags().Int(opt.name, v, opt.description) + case string: + rootCmd.Flags().String(opt.name, v, opt.description) + } + + // Create uppercase env var name + envVar := strings.ReplaceAll(strings.ToUpper(opt.name), "-", "_") + viper.BindEnv(opt.name, envVar) + } viper.BindPFlags(rootCmd.Flags()) } diff --git a/cmd/version.go b/cmd/version.go index ffbd6fce..2a1827b7 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -2,7 +2,7 @@ package cmd import ( "fmt" - "tinyauth/internal/constants" + "tinyauth/internal/config" "github.com/spf13/cobra" ) @@ -12,9 +12,9 @@ var versionCmd = &cobra.Command{ Short: "Print the version number of Tinyauth", Long: `All software has versions. This is Tinyauth's`, Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("Version: %s\n", constants.Version) - fmt.Printf("Commit Hash: %s\n", constants.CommitHash) - fmt.Printf("Build Timestamp: %s\n", constants.BuildTimestamp) + fmt.Printf("Version: %s\n", config.Version) + fmt.Printf("Commit Hash: %s\n", config.CommitHash) + fmt.Printf("Build Timestamp: %s\n", config.BuildTimestamp) }, } diff --git a/frontend/src/context/app-context.tsx b/frontend/src/context/app-context.tsx index 13abf50d..8f76c119 100644 --- a/frontend/src/context/app-context.tsx +++ b/frontend/src/context/app-context.tsx @@ -15,7 +15,7 @@ export const AppContextProvider = ({ }) => { const { isFetching, data, error } = useSuspenseQuery({ queryKey: ["app"], - queryFn: () => axios.get("/api/app").then((res) => res.data), + queryFn: () => axios.get("/api/context/app").then((res) => res.data), }); if (error && !isFetching) { diff --git a/frontend/src/context/user-context.tsx b/frontend/src/context/user-context.tsx index 43b3c005..a3cfeaa2 100644 --- a/frontend/src/context/user-context.tsx +++ b/frontend/src/context/user-context.tsx @@ -15,7 +15,7 @@ export const UserContextProvider = ({ }) => { const { isFetching, data, error } = useSuspenseQuery({ queryKey: ["user"], - queryFn: () => axios.get("/api/user").then((res) => res.data), + queryFn: () => axios.get("/api/context/user").then((res) => res.data), }); if (error && !isFetching) { diff --git a/frontend/src/pages/login-page.tsx b/frontend/src/pages/login-page.tsx index 4828b383..53f183f1 100644 --- a/frontend/src/pages/login-page.tsx +++ b/frontend/src/pages/login-page.tsx @@ -65,7 +65,7 @@ export const LoginPage = () => { }); const loginMutation = useMutation({ - mutationFn: (values: LoginSchema) => axios.post("/api/login", values), + mutationFn: (values: LoginSchema) => axios.post("/api/user/login", values), mutationKey: ["login"], onSuccess: (data) => { if (data.data.totpPending) { diff --git a/frontend/src/pages/logout-page.tsx b/frontend/src/pages/logout-page.tsx index 8c285002..30b2af8c 100644 --- a/frontend/src/pages/logout-page.tsx +++ b/frontend/src/pages/logout-page.tsx @@ -26,7 +26,7 @@ export const LogoutPage = () => { const { t } = useTranslation(); const logoutMutation = useMutation({ - mutationFn: () => axios.post("/api/logout"), + mutationFn: () => axios.post("/api/user/logout"), mutationKey: ["logout"], onSuccess: () => { toast.success(t("logoutSuccessTitle"), { diff --git a/frontend/src/pages/totp-page.tsx b/frontend/src/pages/totp-page.tsx index e04fb2f4..7d4ebad1 100644 --- a/frontend/src/pages/totp-page.tsx +++ b/frontend/src/pages/totp-page.tsx @@ -32,7 +32,7 @@ export const TotpPage = () => { const redirectUri = searchParams.get("redirect_uri"); const totpMutation = useMutation({ - mutationFn: (values: TotpSchema) => axios.post("/api/totp", values), + mutationFn: (values: TotpSchema) => axios.post("/api/user/totp", values), mutationKey: ["totp"], onSuccess: () => { toast.success(t("totpSuccessTitle"), { diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 07e6e7e6..f391a49d 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -19,6 +19,11 @@ export default defineConfig({ changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ""), }, + "/resources": { + target: "http://tinyauth-backend:3000/resources", + changeOrigin: true, + rewrite: (path) => path.replace(/^\/resources/, ""), + }, }, allowedHosts: true, }, diff --git a/go.mod b/go.mod index 0a6f8852..8388b2a3 100644 --- a/go.mod +++ b/go.mod @@ -68,6 +68,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator v9.31.0+incompatible github.com/goccy/go-json v0.10.4 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect diff --git a/go.sum b/go.sum index dabff47e..b43990cb 100644 --- a/go.sum +++ b/go.sum @@ -111,6 +111,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp4Mit+3FDh548oRqwVgNsHA= +github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig= github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= diff --git a/internal/assets/assets.go b/internal/assets/assets.go index 69188674..df6e61f1 100644 --- a/internal/assets/assets.go +++ b/internal/assets/assets.go @@ -4,7 +4,7 @@ import ( "embed" ) -// UI assets +// Frontend assets // //go:embed dist -var Assets embed.FS +var FrontendAssets embed.FS diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go deleted file mode 100644 index 1ab73294..00000000 --- a/internal/auth/auth_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package auth_test - -import ( - "testing" - "time" - "tinyauth/internal/auth" - "tinyauth/internal/types" -) - -var config = types.AuthConfig{ - Users: types.Users{}, - OauthWhitelist: "", - SessionExpiry: 3600, -} - -func TestLoginRateLimiting(t *testing.T) { - // Initialize a new auth service with 3 max retries and 5 seconds timeout - config.LoginMaxRetries = 3 - config.LoginTimeout = 5 - authService := auth.NewAuth(config, nil, nil) - - // Test identifier - identifier := "test_user" - - // Test successful login - should not lock account - t.Log("Testing successful login") - - authService.RecordLoginAttempt(identifier, true) - locked, _ := authService.IsAccountLocked(identifier) - - if locked { - t.Fatalf("Account should not be locked after successful login") - } - - // Test 2 failed attempts - should not lock account yet - t.Log("Testing 2 failed login attempts") - - authService.RecordLoginAttempt(identifier, false) - authService.RecordLoginAttempt(identifier, false) - locked, _ = authService.IsAccountLocked(identifier) - - if locked { - t.Fatalf("Account should not be locked after only 2 failed attempts") - } - - // Add one more failed attempt (total 3) - should lock account with maxRetries=3 - t.Log("Testing 3 failed login attempts") - authService.RecordLoginAttempt(identifier, false) - locked, remainingTime := authService.IsAccountLocked(identifier) - - if !locked { - t.Fatalf("Account should be locked after reaching max retries") - } - if remainingTime <= 0 || remainingTime > 5 { - t.Fatalf("Expected remaining time between 1-5 seconds, got %d", remainingTime) - } - - // Test reset after waiting for timeout - use 1 second timeout for fast testing - t.Log("Testing unlocking after timeout") - - // Reinitialize auth service with a shorter timeout for testing - config.LoginTimeout = 1 - config.LoginMaxRetries = 3 - authService = auth.NewAuth(config, nil, nil) - - // Add enough failed attempts to lock the account - for i := 0; i < 3; i++ { - authService.RecordLoginAttempt(identifier, false) - } - - // Verify it's locked - locked, _ = authService.IsAccountLocked(identifier) - if !locked { - t.Fatalf("Account should be locked initially") - } - - // Wait a bit and verify it gets unlocked after timeout - time.Sleep(1500 * time.Millisecond) // Wait longer than the timeout - locked, _ = authService.IsAccountLocked(identifier) - - if locked { - t.Fatalf("Account should be unlocked after timeout period") - } - - // Test disabled rate limiting - t.Log("Testing disabled rate limiting") - config.LoginMaxRetries = 0 - config.LoginTimeout = 0 - authService = auth.NewAuth(config, nil, nil) - - for i := 0; i < 10; i++ { - authService.RecordLoginAttempt(identifier, false) - } - - locked, _ = authService.IsAccountLocked(identifier) - if locked { - t.Fatalf("Account should not be locked when rate limiting is disabled") - } -} - -func TestConcurrentLoginAttempts(t *testing.T) { - // Initialize a new auth service with 2 max retries and 5 seconds timeout - config.LoginMaxRetries = 2 - config.LoginTimeout = 5 - authService := auth.NewAuth(config, nil, nil) - - // Test multiple identifiers - identifiers := []string{"user1", "user2", "user3"} - - // Test that locking one identifier doesn't affect others - t.Log("Testing multiple identifiers") - - // Add enough failed attempts to lock first user (2 attempts with maxRetries=2) - authService.RecordLoginAttempt(identifiers[0], false) - authService.RecordLoginAttempt(identifiers[0], false) - - // Check if first user is locked - locked, _ := authService.IsAccountLocked(identifiers[0]) - if !locked { - t.Fatalf("User1 should be locked after reaching max retries") - } - - // Check that other users are not affected - for i := 1; i < len(identifiers); i++ { - locked, _ := authService.IsAccountLocked(identifiers[i]) - if locked { - t.Fatalf("User%d should not be locked", i+1) - } - } - - // Test successful login after failed attempts (but before lock) - t.Log("Testing successful login after failed attempts but before lock") - - // One failed attempt for user2 - authService.RecordLoginAttempt(identifiers[1], false) - - // Successful login should reset the counter - authService.RecordLoginAttempt(identifiers[1], true) - - // Now try a failed login again - should not be locked as counter was reset - authService.RecordLoginAttempt(identifiers[1], false) - locked, _ = authService.IsAccountLocked(identifiers[1]) - if locked { - t.Fatalf("User2 should not be locked after successful login reset") - } -} diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go new file mode 100644 index 00000000..594c575f --- /dev/null +++ b/internal/bootstrap/app_bootstrap.go @@ -0,0 +1,260 @@ +package bootstrap + +import ( + "fmt" + "strings" + "tinyauth/internal/config" + "tinyauth/internal/controller" + "tinyauth/internal/middleware" + "tinyauth/internal/service" + "tinyauth/internal/utils" + + "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" +) + +type Controller interface { + SetupRoutes() +} + +type Middleware interface { + Middleware() gin.HandlerFunc + Init() error +} + +type Service interface { + Init() error +} + +type BootstrapApp struct { + Config config.Config +} + +func NewBootstrapApp(config config.Config) *BootstrapApp { + return &BootstrapApp{ + Config: config, + } +} + +func (app *BootstrapApp) Setup() error { + // Parse users + users, err := utils.GetUsers(app.Config.Users, app.Config.UsersFile) + + if err != nil { + return err + } + + // Get domain + domain, err := utils.GetUpperDomain(app.Config.AppURL) + + if err != nil { + return err + } + + // Cookie names + cookieId := utils.GenerateIdentifier(strings.Split(domain, ".")[0]) + sessionCookieName := fmt.Sprintf("%s-%s", config.SessionCookieName, cookieId) + csrfCookieName := fmt.Sprintf("%s-%s", config.CSRFCookieName, cookieId) + redirectCookieName := fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId) + + // Secrets + encryptionSecret, err := utils.DeriveKey(app.Config.Secret, "encryption") + + if err != nil { + return err + } + + hmacSecret, err := utils.DeriveKey(app.Config.Secret, "hmac") + + if err != nil { + return err + } + + // Create configs + authConfig := service.AuthServiceConfig{ + Users: users, + OauthWhitelist: app.Config.OAuthWhitelist, + SessionExpiry: app.Config.SessionExpiry, + SecureCookie: app.Config.SecureCookie, + Domain: domain, + LoginTimeout: app.Config.LoginTimeout, + LoginMaxRetries: app.Config.LoginMaxRetries, + SessionCookieName: sessionCookieName, + HMACSecret: hmacSecret, + EncryptionSecret: encryptionSecret, + } + + // Setup services + var ldapService *service.LdapService + + if app.Config.LdapAddress != "" { + ldapConfig := service.LdapServiceConfig{ + Address: app.Config.LdapAddress, + BindDN: app.Config.LdapBindDN, + BindPassword: app.Config.LdapBindPassword, + BaseDN: app.Config.LdapBaseDN, + Insecure: app.Config.LdapInsecure, + SearchFilter: app.Config.LdapSearchFilter, + } + + ldapService = service.NewLdapService(ldapConfig) + + err := ldapService.Init() + + if err != nil { + log.Warn().Err(err).Msg("Failed to initialize LDAP service, continuing without LDAP") + ldapService = nil + } + } + + dockerService := service.NewDockerService() + authService := service.NewAuthService(authConfig, dockerService, ldapService) + oauthBrokerService := service.NewOAuthBrokerService(app.getOAuthBrokerConfig()) + + // Initialize services + services := []Service{ + dockerService, + authService, + oauthBrokerService, + } + + for _, svc := range services { + if svc != nil { + log.Debug().Str("service", fmt.Sprintf("%T", svc)).Msg("Initializing service") + err := svc.Init() + if err != nil { + return err + } + } + } + + // Configured providers + var configuredProviders []string + + if authService.UserAuthConfigured() || ldapService != nil { + configuredProviders = append(configuredProviders, "username") + } + + configuredProviders = append(configuredProviders, oauthBrokerService.GetConfiguredServices()...) + + if len(configuredProviders) == 0 { + return fmt.Errorf("no authentication providers configured") + } + + // Create engine + engine := gin.New() + + if config.Version != "development" { + gin.SetMode(gin.ReleaseMode) + } + + // Create middlewares + var middlewares []Middleware + + contextMiddleware := middleware.NewContextMiddleware(middleware.ContextMiddlewareConfig{ + Domain: domain, + }, authService, oauthBrokerService) + + uiMiddleware := middleware.NewUIMiddleware() + zerologMiddleware := middleware.NewZerologMiddleware() + + middlewares = append(middlewares, contextMiddleware, uiMiddleware, zerologMiddleware) + + for _, middleware := range middlewares { + log.Debug().Str("middleware", fmt.Sprintf("%T", middleware)).Msg("Initializing middleware") + err := middleware.Init() + if err != nil { + return fmt.Errorf("failed to initialize middleware %T: %w", middleware, err) + } + engine.Use(middleware.Middleware()) + } + + // Create routers + mainRouter := engine.Group("") + apiRouter := engine.Group("/api") + + // Create controllers + contextController := controller.NewContextController(controller.ContextControllerConfig{ + ConfiguredProviders: configuredProviders, + DisableContinue: app.Config.DisableContinue, + Title: app.Config.Title, + GenericName: app.Config.GenericName, + Domain: domain, + ForgotPasswordMessage: app.Config.FogotPasswordMessage, + BackgroundImage: app.Config.BackgroundImage, + OAuthAutoRedirect: app.Config.OAuthAutoRedirect, + }, apiRouter) + + oauthController := controller.NewOAuthController(controller.OAuthControllerConfig{ + AppURL: app.Config.AppURL, + SecureCookie: app.Config.SecureCookie, + CSRFCookieName: csrfCookieName, + RedirectCookieName: redirectCookieName, + Domain: domain, + }, apiRouter, authService, oauthBrokerService) + + proxyController := controller.NewProxyController(controller.ProxyControllerConfig{ + AppURL: app.Config.AppURL, + }, apiRouter, dockerService, authService) + + userController := controller.NewUserController(controller.UserControllerConfig{ + Domain: domain, + }, apiRouter, authService) + + resourcesController := controller.NewResourcesController(controller.ResourcesControllerConfig{ + ResourcesDir: app.Config.ResourcesDir, + }, mainRouter) + + healthController := controller.NewHealthController(apiRouter) + + // Setup routes + controller := []Controller{ + contextController, + oauthController, + proxyController, + userController, + healthController, + resourcesController, + } + + for _, ctrl := range controller { + log.Debug().Msgf("Setting up %T controller", ctrl) + ctrl.SetupRoutes() + } + + // Start server + address := fmt.Sprintf("%s:%d", app.Config.Address, app.Config.Port) + log.Info().Msgf("Starting server on %s", address) + if err := engine.Run(address); err != nil { + log.Fatal().Err(err).Msg("Failed to start server") + } + + return nil +} + +// Temporary +func (app *BootstrapApp) getOAuthBrokerConfig() map[string]config.OAuthServiceConfig { + return map[string]config.OAuthServiceConfig{ + "google": { + ClientID: app.Config.GoogleClientId, + ClientSecret: app.Config.GoogleClientSecret, + RedirectURL: fmt.Sprintf("%s/api/oauth/callback/google", app.Config.AppURL), + }, + "github": { + ClientID: app.Config.GithubClientId, + ClientSecret: app.Config.GithubClientSecret, + RedirectURL: fmt.Sprintf("%s/api/oauth/callback/github", app.Config.AppURL), + }, + "generic": { + ClientID: app.Config.GenericClientId, + ClientSecret: app.Config.GenericClientSecret, + RedirectURL: fmt.Sprintf("%s/api/oauth/callback/generic", app.Config.AppURL), + Scopes: strings.Split(app.Config.GenericScopes, ","), + AuthURL: app.Config.GenericAuthURL, + TokenURL: app.Config.GenericTokenURL, + UserinfoURL: app.Config.GenericUserURL, + InsecureSkipVerify: app.Config.GenericSkipSSL, + }, + } + +} diff --git a/internal/types/config.go b/internal/config/config.go similarity index 56% rename from internal/types/config.go rename to internal/config/config.go index b53e0536..5d4dba86 100644 --- a/internal/types/config.go +++ b/internal/config/config.go @@ -1,6 +1,20 @@ -package types +package config + +type Claims struct { + Name string `json:"name"` + Email string `json:"email"` + PreferredUsername string `json:"preferred_username"` + Groups any `json:"groups"` +} + +var Version = "development" +var CommitHash = "n/a" +var BuildTimestamp = "n/a" + +var SessionCookieName = "tinyauth-session" +var CSRFCookieName = "tinyauth-csrf" +var RedirectCookieName = "tinyauth-redirect" -// Config is the configuration for the tinyauth server type Config struct { Port int `mapstructure:"port" validate:"required"` Address string `validate:"required,ip4_addr" mapstructure:"address"` @@ -9,7 +23,7 @@ type Config struct { AppURL string `validate:"required,url" mapstructure:"app-url"` Users string `mapstructure:"users"` UsersFile string `mapstructure:"users-file"` - CookieSecure bool `mapstructure:"cookie-secure"` + SecureCookie bool `mapstructure:"secure-cookie"` GithubClientId string `mapstructure:"github-client-id"` GithubClientSecret string `mapstructure:"github-client-secret"` GithubClientSecretFile string `mapstructure:"github-client-secret-file"` @@ -29,9 +43,8 @@ type Config struct { OAuthWhitelist string `mapstructure:"oauth-whitelist"` OAuthAutoRedirect string `mapstructure:"oauth-auto-redirect" validate:"oneof=none github google generic"` SessionExpiry int `mapstructure:"session-expiry"` - LogLevel int8 `mapstructure:"log-level" validate:"min=-1,max=5"` + LogLevel string `mapstructure:"log-level" validate:"oneof=trace debug info warn error fatal panic"` Title string `mapstructure:"app-title"` - EnvFile string `mapstructure:"env-file"` LoginTimeout int `mapstructure:"login-timeout"` LoginMaxRetries int `mapstructure:"login-max-retries"` FogotPasswordMessage string `mapstructure:"forgot-password-message"` @@ -42,90 +55,30 @@ type Config struct { LdapBaseDN string `mapstructure:"ldap-base-dn"` LdapInsecure bool `mapstructure:"ldap-insecure"` LdapSearchFilter string `mapstructure:"ldap-search-filter"` + ResourcesDir string `mapstructure:"resources-dir"` } -// Server configuration -type HandlersConfig struct { - AppURL string - Domain string - CookieSecure bool - DisableContinue bool - GenericName string - Title string - ForgotPasswordMessage string - BackgroundImage string - OAuthAutoRedirect string - CsrfCookieName string - RedirectCookieName string -} - -// OAuthConfig is the configuration for the providers -type OAuthConfig struct { - GithubClientId string - GithubClientSecret string - GoogleClientId string - GoogleClientSecret string - GenericClientId string - GenericClientSecret string - GenericScopes []string - GenericAuthURL string - GenericTokenURL string - GenericUserURL string - GenericSkipSSL bool - AppURL string -} - -// ServerConfig is the configuration for the server -type ServerConfig struct { - Port int - Address string -} - -// AuthConfig is the configuration for the auth service -type AuthConfig struct { - Users Users - OauthWhitelist string - SessionExpiry int - CookieSecure bool - Domain string - LoginTimeout int - LoginMaxRetries int - SessionCookieName string - HMACSecret string - EncryptionSecret string -} - -// HooksConfig is the configuration for the hooks service -type HooksConfig struct { - Domain string -} - -// OAuthLabels is a list of labels that can be used in a tinyauth protected container type OAuthLabels struct { Whitelist string Groups string } -// Basic auth labels for a tinyauth protected container type BasicLabels struct { Username string - Password PassowrdLabels + Password PasswordLabels } -// PassowrdLabels is a struct that contains the password labels for a tinyauth protected container -type PassowrdLabels struct { +type PasswordLabels struct { Plain string File string } -// IP labels for a tinyauth protected container type IPLabels struct { Allow []string Block []string Bypass []string } -// Labels is a struct that contains the labels for a tinyauth protected container type Labels struct { Users string Allowed string @@ -136,12 +89,56 @@ type Labels struct { IP IPLabels } -// Ldap config is a struct that contains the configuration for the LDAP service -type LdapConfig struct { - Address string - BindDN string - BindPassword string - BaseDN string - Insecure bool - SearchFilter string +type OAuthServiceConfig struct { + ClientID string + ClientSecret string + Scopes []string + RedirectURL string + AuthURL string + TokenURL string + UserinfoURL string + InsecureSkipVerify bool +} + +type User struct { + Username string + Password string + TotpSecret string +} + +type UserSearch struct { + Username string + Type string // local, ldap or unknown +} + +type SessionCookie struct { + Username string + Name string + Email string + Provider string + TotpPending bool + OAuthGroups string +} + +type UserContext struct { + Username string + Name string + Email string + IsLoggedIn bool + OAuth bool + Provider string + TotpPending bool + OAuthGroups string + TotpEnabled bool +} + +type UnauthorizedQuery struct { + Username string `url:"username"` + Resource string `url:"resource"` + GroupErr bool `url:"groupErr"` + IP string `url:"ip"` +} + +type RedirectQuery struct { + RedirectURI string `url:"redirect_uri"` } diff --git a/internal/constants/constants.go b/internal/constants/constants.go deleted file mode 100644 index d6f64fab..00000000 --- a/internal/constants/constants.go +++ /dev/null @@ -1,19 +0,0 @@ -package constants - -// Claims are the OIDC supported claims (prefered username is included for convinience) -type Claims struct { - Name string `json:"name"` - Email string `json:"email"` - PreferredUsername string `json:"preferred_username"` - Groups any `json:"groups"` -} - -// Version information -var Version = "development" -var CommitHash = "n/a" -var BuildTimestamp = "n/a" - -// Base cookie names -var SessionCookieName = "tinyauth-session" -var CsrfCookieName = "tinyauth-csrf" -var RedirectCookieName = "tinyauth-redirect" diff --git a/internal/controller/context_controller.go b/internal/controller/context_controller.go new file mode 100644 index 00000000..c7570f0e --- /dev/null +++ b/internal/controller/context_controller.go @@ -0,0 +1,104 @@ +package controller + +import ( + "tinyauth/internal/utils" + + "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" +) + +type UserContextResponse struct { + Status int `json:"status"` + Message string `json:"message"` + IsLoggedIn bool `json:"isLoggedIn"` + Username string `json:"username"` + Name string `json:"name"` + Email string `json:"email"` + Provider string `json:"provider"` + Oauth bool `json:"oauth"` + TotpPending bool `json:"totpPending"` +} + +type AppContextResponse struct { + Status int `json:"status"` + Message string `json:"message"` + ConfiguredProviders []string `json:"configuredProviders"` + DisableContinue bool `json:"disableContinue"` + Title string `json:"title"` + GenericName string `json:"genericName"` + Domain string `json:"domain"` + ForgotPasswordMessage string `json:"forgotPasswordMessage"` + BackgroundImage string `json:"backgroundImage"` + OAuthAutoRedirect string `json:"oauthAutoRedirect"` +} + +type ContextControllerConfig struct { + ConfiguredProviders []string + DisableContinue bool + Title string + GenericName string + Domain string + ForgotPasswordMessage string + BackgroundImage string + OAuthAutoRedirect string +} + +type ContextController struct { + Config ContextControllerConfig + Router *gin.RouterGroup +} + +func NewContextController(config ContextControllerConfig, router *gin.RouterGroup) *ContextController { + return &ContextController{ + Config: config, + Router: router, + } +} + +func (controller *ContextController) SetupRoutes() { + contextGroup := controller.Router.Group("/context") + contextGroup.GET("/user", controller.userContextHandler) + contextGroup.GET("/app", controller.appContextHandler) +} + +func (controller *ContextController) userContextHandler(c *gin.Context) { + context, err := utils.GetContext(c) + + userContext := UserContextResponse{ + Status: 200, + Message: "Success", + IsLoggedIn: context.IsLoggedIn, + Username: context.Username, + Name: context.Name, + Email: context.Email, + Provider: context.Provider, + Oauth: context.OAuth, + TotpPending: context.TotpPending, + } + + if err != nil { + log.Debug().Err(err).Msg("No user context found in request") + userContext.Status = 401 + userContext.Message = "Unauthorized" + userContext.IsLoggedIn = false + c.JSON(200, userContext) + return + } + + c.JSON(200, userContext) +} + +func (controller *ContextController) appContextHandler(c *gin.Context) { + c.JSON(200, AppContextResponse{ + Status: 200, + Message: "Success", + ConfiguredProviders: controller.Config.ConfiguredProviders, + DisableContinue: controller.Config.DisableContinue, + Title: controller.Config.Title, + GenericName: controller.Config.GenericName, + Domain: controller.Config.Domain, + ForgotPasswordMessage: controller.Config.ForgotPasswordMessage, + BackgroundImage: controller.Config.BackgroundImage, + OAuthAutoRedirect: controller.Config.OAuthAutoRedirect, + }) +} diff --git a/internal/controller/health_controller.go b/internal/controller/health_controller.go new file mode 100644 index 00000000..842b3d30 --- /dev/null +++ b/internal/controller/health_controller.go @@ -0,0 +1,25 @@ +package controller + +import "github.com/gin-gonic/gin" + +type HealthController struct { + Router *gin.RouterGroup +} + +func NewHealthController(router *gin.RouterGroup) *HealthController { + return &HealthController{ + Router: router, + } +} + +func (controller *HealthController) SetupRoutes() { + controller.Router.GET("/health", controller.healthHandler) + controller.Router.HEAD("/health", controller.healthHandler) +} + +func (controller *HealthController) healthHandler(c *gin.Context) { + c.JSON(200, gin.H{ + "status": "ok", + "message": "Healthy", + }) +} diff --git a/internal/controller/oauth_controller.go b/internal/controller/oauth_controller.go new file mode 100644 index 00000000..aa3289bb --- /dev/null +++ b/internal/controller/oauth_controller.go @@ -0,0 +1,200 @@ +package controller + +import ( + "fmt" + "net/http" + "strings" + "time" + "tinyauth/internal/config" + "tinyauth/internal/service" + "tinyauth/internal/utils" + + "github.com/gin-gonic/gin" + "github.com/google/go-querystring/query" + "github.com/rs/zerolog/log" +) + +type OAuthRequest struct { + Provider string `uri:"provider" binding:"required"` +} + +type OAuthControllerConfig struct { + CSRFCookieName string + RedirectCookieName string + SecureCookie bool + AppURL string + Domain string +} + +type OAuthController struct { + Config OAuthControllerConfig + Router *gin.RouterGroup + Auth *service.AuthService + Broker *service.OAuthBrokerService +} + +func NewOAuthController(config OAuthControllerConfig, router *gin.RouterGroup, auth *service.AuthService, broker *service.OAuthBrokerService) *OAuthController { + return &OAuthController{ + Config: config, + Router: router, + Auth: auth, + Broker: broker, + } +} + +func (controller *OAuthController) SetupRoutes() { + oauthGroup := controller.Router.Group("/oauth") + oauthGroup.GET("/url/:provider", controller.oauthURLHandler) + oauthGroup.GET("/callback/:provider", controller.oauthCallbackHandler) +} + +func (controller *OAuthController) oauthURLHandler(c *gin.Context) { + var req OAuthRequest + + err := c.BindUri(&req) + if err != nil { + log.Error().Err(err).Msg("Failed to bind URI") + c.JSON(400, gin.H{ + "status": 400, + "message": "Bad Request", + }) + return + } + + service, exists := controller.Broker.GetService(req.Provider) + + if !exists { + log.Warn().Msgf("OAuth provider not found: %s", req.Provider) + c.JSON(404, gin.H{ + "status": 404, + "message": "Not Found", + }) + return + } + + state := service.GenerateState() + authURL := service.GetAuthURL(state) + c.SetCookie(controller.Config.CSRFCookieName, state, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.Config.Domain), controller.Config.SecureCookie, true) + + redirectURI := c.Query("redirect_uri") + + if redirectURI != "" && utils.IsRedirectSafe(redirectURI, controller.Config.Domain) { + log.Debug().Msg("Setting redirect URI cookie") + c.SetCookie(controller.Config.RedirectCookieName, redirectURI, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.Config.Domain), controller.Config.SecureCookie, true) + } + + c.JSON(200, gin.H{ + "status": 200, + "message": "OK", + "url": authURL, + }) +} + +func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { + var req OAuthRequest + + err := c.BindUri(&req) + if err != nil { + log.Error().Err(err).Msg("Failed to bind URI") + c.JSON(400, gin.H{ + "status": 400, + "message": "Bad Request", + }) + return + } + + state := c.Query("state") + csrfCookie, err := c.Cookie(controller.Config.CSRFCookieName) + + if err != nil || state != csrfCookie { + log.Warn().Err(err).Msg("CSRF token mismatch or cookie missing") + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) + return + } + + c.SetCookie(controller.Config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.Config.Domain), controller.Config.SecureCookie, true) + + code := c.Query("code") + service, exists := controller.Broker.GetService(req.Provider) + + if !exists { + log.Warn().Msgf("OAuth provider not found: %s", req.Provider) + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) + return + } + + err = service.VerifyCode(code) + if err != nil { + log.Error().Err(err).Msg("Failed to verify OAuth code") + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) + return + } + + user, err := controller.Broker.GetUser(req.Provider) + + if err != nil { + log.Error().Err(err).Msg("Failed to get user from OAuth provider") + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) + return + } + + if user.Email == "" { + log.Error().Msg("OAuth provider did not return an email") + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) + return + } + + if !controller.Auth.EmailWhitelisted(user.Email) { + queries, err := query.Values(config.UnauthorizedQuery{ + Username: user.Email, + }) + + if err != nil { + log.Error().Err(err).Msg("Failed to encode unauthorized query") + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) + return + } + + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.Config.AppURL, queries.Encode())) + return + } + + var name string + + if user.Name != "" { + log.Debug().Msg("Using name from OAuth provider") + name = user.Name + } else { + log.Debug().Msg("No name from OAuth provider, using pseudo name") + name = fmt.Sprintf("%s (%s)", utils.Capitalize(strings.Split(user.Email, "@")[0]), strings.Split(user.Email, "@")[1]) + } + + controller.Auth.CreateSessionCookie(c, &config.SessionCookie{ + Username: user.Email, + Name: name, + Email: user.Email, + Provider: req.Provider, + OAuthGroups: utils.CoalesceToString(user.Groups), + }) + + redirectURI, err := c.Cookie(controller.Config.RedirectCookieName) + + if err != nil || !utils.IsRedirectSafe(redirectURI, controller.Config.Domain) { + log.Debug().Msg("No redirect URI cookie found, redirecting to app root") + c.Redirect(http.StatusTemporaryRedirect, controller.Config.AppURL) + return + } + + queries, err := query.Values(config.RedirectQuery{ + RedirectURI: redirectURI, + }) + + if err != nil { + log.Error().Err(err).Msg("Failed to encode redirect URI query") + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) + return + } + + c.SetCookie(controller.Config.RedirectCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.Config.Domain), controller.Config.SecureCookie, true) + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/continue?%s", controller.Config.AppURL, queries.Encode())) +} diff --git a/internal/controller/proxy_controller.go b/internal/controller/proxy_controller.go new file mode 100644 index 00000000..348be65b --- /dev/null +++ b/internal/controller/proxy_controller.go @@ -0,0 +1,311 @@ +package controller + +import ( + "fmt" + "net/http" + "strings" + "tinyauth/internal/config" + "tinyauth/internal/service" + "tinyauth/internal/utils" + + "github.com/gin-gonic/gin" + "github.com/google/go-querystring/query" + "github.com/rs/zerolog/log" +) + +type Proxy struct { + Proxy string `uri:"proxy" binding:"required"` +} + +type ProxyControllerConfig struct { + AppURL string +} + +type ProxyController struct { + Config ProxyControllerConfig + Router *gin.RouterGroup + Docker *service.DockerService + Auth *service.AuthService +} + +func NewProxyController(config ProxyControllerConfig, router *gin.RouterGroup, docker *service.DockerService, auth *service.AuthService) *ProxyController { + return &ProxyController{ + Config: config, + Router: router, + Docker: docker, + Auth: auth, + } +} + +func (controller *ProxyController) SetupRoutes() { + proxyGroup := controller.Router.Group("/auth") + proxyGroup.GET("/:proxy", controller.proxyHandler) +} + +func (controller *ProxyController) proxyHandler(c *gin.Context) { + var req Proxy + + err := c.BindUri(&req) + if err != nil { + log.Error().Err(err).Msg("Failed to bind URI") + c.JSON(400, gin.H{ + "status": 400, + "message": "Bad Request", + }) + return + } + + isBrowser := strings.Contains(c.Request.Header.Get("Accept"), "text/html") + + if isBrowser { + log.Debug().Msg("Request identified as (most likely) coming from a browser") + } else { + log.Debug().Msg("Request identified as (most likely) coming from a non-browser client") + } + + uri := c.Request.Header.Get("X-Forwarded-Uri") + proto := c.Request.Header.Get("X-Forwarded-Proto") + host := c.Request.Header.Get("X-Forwarded-Host") + + hostWithoutPort := strings.Split(host, ":")[0] + id := strings.Split(hostWithoutPort, ".")[0] + + labels, err := controller.Docker.GetLabels(id, hostWithoutPort) + + if err != nil { + log.Error().Err(err).Msg("Failed to get labels from Docker") + + if req.Proxy == "nginx" || !isBrowser { + c.JSON(500, gin.H{ + "status": 500, + "message": "Internal Server Error", + }) + return + } + + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) + return + } + + clientIP := c.ClientIP() + + if controller.Auth.BypassedIP(labels, clientIP) { + c.Header("Authorization", c.Request.Header.Get("Authorization")) + + headers := utils.ParseHeaders(labels.Headers) + + for key, value := range headers { + log.Debug().Str("header", key).Msg("Setting header") + c.Header(key, value) + } + + if labels.Basic.Username != "" && utils.GetSecret(labels.Basic.Password.Plain, labels.Basic.Password.File) != "" { + log.Debug().Str("username", labels.Basic.Username).Msg("Setting basic auth header") + c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(labels.Basic.Username, utils.GetSecret(labels.Basic.Password.Plain, labels.Basic.Password.File)))) + } + + c.JSON(200, gin.H{ + "status": 200, + "message": "Authenticated", + }) + return + } + + if !controller.Auth.CheckIP(labels, clientIP) { + if req.Proxy == "nginx" || !isBrowser { + c.JSON(401, gin.H{ + "status": 401, + "message": "Unauthorized", + }) + return + } + + queries, err := query.Values(config.UnauthorizedQuery{ + Resource: strings.Split(host, ".")[0], + IP: clientIP, + }) + + if err != nil { + log.Error().Err(err).Msg("Failed to encode unauthorized query") + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) + return + } + + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.Config.AppURL, queries.Encode())) + return + } + + authEnabled, err := controller.Auth.AuthEnabled(uri, labels) + + if err != nil { + log.Error().Err(err).Msg("Failed to check if auth is enabled for resource") + + if req.Proxy == "nginx" || !isBrowser { + c.JSON(500, gin.H{ + "status": 500, + "message": "Internal Server Error", + }) + return + } + + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) + return + } + + if !authEnabled { + log.Debug().Msg("Authentication disabled for resource, allowing access") + + c.Header("Authorization", c.Request.Header.Get("Authorization")) + + headers := utils.ParseHeaders(labels.Headers) + + for key, value := range headers { + log.Debug().Str("header", key).Msg("Setting header") + c.Header(key, value) + } + + if labels.Basic.Username != "" && utils.GetSecret(labels.Basic.Password.Plain, labels.Basic.Password.File) != "" { + log.Debug().Str("username", labels.Basic.Username).Msg("Setting basic auth header") + c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(labels.Basic.Username, utils.GetSecret(labels.Basic.Password.Plain, labels.Basic.Password.File)))) + } + + c.JSON(200, gin.H{ + "status": 200, + "message": "Authenticated", + }) + return + } + + var userContext config.UserContext + + context, err := utils.GetContext(c) + + if err != nil { + log.Debug().Msg("No user context found in request, treating as not logged in") + userContext = config.UserContext{ + IsLoggedIn: false, + } + } else { + userContext = context + } + + if userContext.Provider == "basic" && userContext.TotpEnabled { + log.Debug().Msg("User has TOTP enabled, denying basic auth access") + userContext.IsLoggedIn = false + } + + if userContext.IsLoggedIn { + appAllowed := controller.Auth.ResourceAllowed(c, userContext, labels) + + if !appAllowed { + log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource") + + if req.Proxy == "nginx" || !isBrowser { + c.JSON(403, gin.H{ + "status": 403, + "message": "Forbidden", + }) + return + } + + queries, err := query.Values(config.UnauthorizedQuery{ + Resource: strings.Split(host, ".")[0], + }) + + if userContext.OAuth { + queries.Set("username", userContext.Email) + } else { + queries.Set("username", userContext.Username) + } + + if err != nil { + log.Error().Err(err).Msg("Failed to encode unauthorized query") + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) + return + } + + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.Config.AppURL, queries.Encode())) + return + } + + if userContext.OAuth { + groupOK := controller.Auth.OAuthGroup(c, userContext, labels) + + if !groupOK { + log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements") + + if req.Proxy == "nginx" || !isBrowser { + c.JSON(403, gin.H{ + "status": 403, + "message": "Forbidden", + }) + return + } + + queries, err := query.Values(config.UnauthorizedQuery{ + Resource: strings.Split(host, ".")[0], + GroupErr: true, + }) + + if userContext.OAuth { + queries.Set("username", userContext.Email) + } else { + queries.Set("username", userContext.Username) + } + + if err != nil { + log.Error().Err(err).Msg("Failed to encode unauthorized query") + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) + return + } + + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.Config.AppURL, queries.Encode())) + return + } + } + + c.Header("Authorization", c.Request.Header.Get("Authorization")) + c.Header("Remote-User", utils.SanitizeHeader(userContext.Username)) + c.Header("Remote-Name", utils.SanitizeHeader(userContext.Name)) + c.Header("Remote-Email", utils.SanitizeHeader(userContext.Email)) + c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups)) + + headers := utils.ParseHeaders(labels.Headers) + + for key, value := range headers { + log.Debug().Str("header", key).Msg("Setting header") + c.Header(key, value) + } + + if labels.Basic.Username != "" && utils.GetSecret(labels.Basic.Password.Plain, labels.Basic.Password.File) != "" { + log.Debug().Str("username", labels.Basic.Username).Msg("Setting basic auth header") + c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(labels.Basic.Username, utils.GetSecret(labels.Basic.Password.Plain, labels.Basic.Password.File)))) + } + + c.JSON(200, gin.H{ + "status": 200, + "message": "Authenticated", + }) + return + } + + if req.Proxy == "nginx" || !isBrowser { + c.JSON(401, gin.H{ + "status": 401, + "message": "Unauthorized", + }) + return + } + + queries, err := query.Values(config.RedirectQuery{ + RedirectURI: fmt.Sprintf("%s://%s%s", proto, host, uri), + }) + + if err != nil { + log.Error().Err(err).Msg("Failed to encode redirect URI query") + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) + return + } + + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/login?%s", controller.Config.AppURL, queries.Encode())) +} diff --git a/internal/controller/resources_controller.go b/internal/controller/resources_controller.go new file mode 100644 index 00000000..56bae87d --- /dev/null +++ b/internal/controller/resources_controller.go @@ -0,0 +1,42 @@ +package controller + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +type ResourcesControllerConfig struct { + ResourcesDir string +} + +type ResourcesController struct { + Config ResourcesControllerConfig + Router *gin.RouterGroup + FileServer http.Handler +} + +func NewResourcesController(config ResourcesControllerConfig, router *gin.RouterGroup) *ResourcesController { + fileServer := http.StripPrefix("/resources", http.FileServer(http.Dir(config.ResourcesDir))) + + return &ResourcesController{ + Config: config, + Router: router, + FileServer: fileServer, + } +} + +func (controller *ResourcesController) SetupRoutes() { + controller.Router.GET("/resources/*resource", controller.resourcesHandler) +} + +func (controller *ResourcesController) resourcesHandler(c *gin.Context) { + if controller.Config.ResourcesDir == "" { + c.JSON(404, gin.H{ + "status": 404, + "message": "Resources not found", + }) + return + } + controller.FileServer.ServeHTTP(c.Writer, c.Request) +} diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go new file mode 100644 index 00000000..f7f7c9e6 --- /dev/null +++ b/internal/controller/user_controller.go @@ -0,0 +1,266 @@ +package controller + +import ( + "fmt" + "strings" + "tinyauth/internal/config" + "tinyauth/internal/service" + "tinyauth/internal/utils" + + "github.com/gin-gonic/gin" + "github.com/pquerna/otp/totp" + "github.com/rs/zerolog/log" +) + +type LoginRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type TotpRequest struct { + Code string `json:"code"` +} + +type UserControllerConfig struct { + Domain string +} + +type UserController struct { + Config UserControllerConfig + Router *gin.RouterGroup + Auth *service.AuthService +} + +func NewUserController(config UserControllerConfig, router *gin.RouterGroup, auth *service.AuthService) *UserController { + return &UserController{ + Config: config, + Router: router, + Auth: auth, + } +} + +func (controller *UserController) SetupRoutes() { + userGroup := controller.Router.Group("/user") + userGroup.POST("/login", controller.loginHandler) + userGroup.POST("/logout", controller.logoutHandler) + userGroup.POST("/totp", controller.totpHandler) +} + +func (controller *UserController) loginHandler(c *gin.Context) { + var req LoginRequest + + err := c.ShouldBindJSON(&req) + if err != nil { + log.Error().Err(err).Msg("Failed to bind JSON") + c.JSON(400, gin.H{ + "status": 400, + "message": "Bad Request", + }) + return + } + + clientIP := c.ClientIP() + + rateIdentifier := req.Username + + if rateIdentifier == "" { + rateIdentifier = clientIP + } + + log.Debug().Str("username", req.Username).Str("ip", clientIP).Msg("Login attempt") + + isLocked, remainingTime := controller.Auth.IsAccountLocked(rateIdentifier) + + if isLocked { + log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("Account is locked due to too many failed login attempts") + c.JSON(429, gin.H{ + "status": 429, + "message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remainingTime), + }) + return + } + + userSearch := controller.Auth.SearchUser(req.Username) + + if userSearch.Type == "" { + log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("User not found") + controller.Auth.RecordLoginAttempt(rateIdentifier, false) + c.JSON(401, gin.H{ + "status": 401, + "message": "Unauthorized", + }) + return + } + + if !controller.Auth.VerifyUser(userSearch, req.Password) { + log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("Invalid password") + controller.Auth.RecordLoginAttempt(rateIdentifier, false) + c.JSON(401, gin.H{ + "status": 401, + "message": "Unauthorized", + }) + return + } + + log.Info().Str("username", req.Username).Str("ip", clientIP).Msg("Login successful") + + controller.Auth.RecordLoginAttempt(rateIdentifier, true) + + if userSearch.Type == "local" { + user := controller.Auth.GetLocalUser(userSearch.Username) + + if user.TotpSecret != "" { + log.Debug().Str("username", req.Username).Msg("User has TOTP enabled, requiring TOTP verification") + + err := controller.Auth.CreateSessionCookie(c, &config.SessionCookie{ + Username: user.Username, + Name: utils.Capitalize(req.Username), + Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.Config.Domain), + Provider: "username", + TotpPending: true, + }) + + if err != nil { + log.Error().Err(err).Msg("Failed to create session cookie") + c.JSON(500, gin.H{ + "status": 500, + "message": "Internal Server Error", + }) + return + } + + c.JSON(200, gin.H{ + "status": 200, + "message": "TOTP required", + "totpPending": true, + }) + return + } + } + + err = controller.Auth.CreateSessionCookie(c, &config.SessionCookie{ + Username: req.Username, + Name: utils.Capitalize(req.Username), + Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.Config.Domain), + Provider: "username", + }) + + if err != nil { + log.Error().Err(err).Msg("Failed to create session cookie") + c.JSON(500, gin.H{ + "status": 500, + "message": "Internal Server Error", + }) + return + } + + c.JSON(200, gin.H{ + "status": 200, + "message": "Login successful", + }) +} + +func (controller *UserController) logoutHandler(c *gin.Context) { + log.Debug().Msg("Logout request received") + + controller.Auth.DeleteSessionCookie(c) + + c.JSON(200, gin.H{ + "status": 200, + "message": "Logout successful", + }) +} + +func (controller *UserController) totpHandler(c *gin.Context) { + var req TotpRequest + + err := c.ShouldBindJSON(&req) + if err != nil { + log.Error().Err(err).Msg("Failed to bind JSON") + c.JSON(400, gin.H{ + "status": 400, + "message": "Bad Request", + }) + return + } + + context, err := utils.GetContext(c) + + if err != nil { + log.Error().Err(err).Msg("Failed to get user context") + c.JSON(500, gin.H{ + "status": 500, + "message": "Internal Server Error", + }) + return + } + + if !context.TotpPending { + log.Warn().Msg("TOTP attempt without a pending TOTP session") + c.JSON(401, gin.H{ + "status": 401, + "message": "Unauthorized", + }) + return + } + + clientIP := c.ClientIP() + + rateIdentifier := context.Username + + if rateIdentifier == "" { + rateIdentifier = clientIP + } + + log.Debug().Str("username", context.Username).Str("ip", clientIP).Msg("TOTP verification attempt") + + isLocked, remainingTime := controller.Auth.IsAccountLocked(rateIdentifier) + + if isLocked { + log.Warn().Str("username", context.Username).Str("ip", clientIP).Msg("Account is locked due to too many failed TOTP attempts") + c.JSON(429, gin.H{ + "status": 429, + "message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remainingTime), + }) + return + } + + user := controller.Auth.GetLocalUser(context.Username) + + ok := totp.Validate(req.Code, user.TotpSecret) + + if !ok { + log.Warn().Str("username", context.Username).Str("ip", clientIP).Msg("Invalid TOTP code") + controller.Auth.RecordLoginAttempt(rateIdentifier, false) + c.JSON(401, gin.H{ + "status": 401, + "message": "Unauthorized", + }) + return + } + + log.Info().Str("username", context.Username).Str("ip", clientIP).Msg("TOTP verification successful") + + controller.Auth.RecordLoginAttempt(rateIdentifier, true) + + err = controller.Auth.CreateSessionCookie(c, &config.SessionCookie{ + Username: user.Username, + Name: utils.Capitalize(user.Username), + Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), controller.Config.Domain), + Provider: "username", + }) + + if err != nil { + log.Error().Err(err).Msg("Failed to create session cookie") + c.JSON(500, gin.H{ + "status": 500, + "message": "Internal Server Error", + }) + return + } + + c.JSON(200, gin.H{ + "status": 200, + "message": "Login successful", + }) +} diff --git a/internal/handlers/context.go b/internal/handlers/context.go deleted file mode 100644 index d0fff5e5..00000000 --- a/internal/handlers/context.go +++ /dev/null @@ -1,64 +0,0 @@ -package handlers - -import ( - "tinyauth/internal/types" - - "github.com/gin-gonic/gin" - "github.com/rs/zerolog/log" -) - -func (h *Handlers) AppContextHandler(c *gin.Context) { - log.Debug().Msg("Getting app context") - - // Get configured providers - configuredProviders := h.Providers.GetConfiguredProviders() - - // We have username/password configured so add it to our providers - if h.Auth.UserAuthConfigured() { - configuredProviders = append(configuredProviders, "username") - } - - // Return app context - appContext := types.AppContext{ - Status: 200, - Message: "OK", - ConfiguredProviders: configuredProviders, - DisableContinue: h.Config.DisableContinue, - Title: h.Config.Title, - GenericName: h.Config.GenericName, - Domain: h.Config.Domain, - ForgotPasswordMessage: h.Config.ForgotPasswordMessage, - BackgroundImage: h.Config.BackgroundImage, - OAuthAutoRedirect: h.Config.OAuthAutoRedirect, - } - c.JSON(200, appContext) -} - -func (h *Handlers) UserContextHandler(c *gin.Context) { - log.Debug().Msg("Getting user context") - - // Create user context using hooks - userContext := h.Hooks.UseUserContext(c) - - userContextResponse := types.UserContextResponse{ - Status: 200, - IsLoggedIn: userContext.IsLoggedIn, - Username: userContext.Username, - Name: userContext.Name, - Email: userContext.Email, - Provider: userContext.Provider, - Oauth: userContext.OAuth, - TotpPending: userContext.TotpPending, - } - - // If we are not logged in we set the status to 401 else we set it to 200 - if !userContext.IsLoggedIn { - log.Debug().Msg("Unauthorized") - userContextResponse.Message = "Unauthorized" - } else { - log.Debug().Interface("userContext", userContext).Msg("Authenticated") - userContextResponse.Message = "Authenticated" - } - - c.JSON(200, userContextResponse) -} diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go deleted file mode 100644 index 0e8ebe22..00000000 --- a/internal/handlers/handlers.go +++ /dev/null @@ -1,36 +0,0 @@ -package handlers - -import ( - "tinyauth/internal/auth" - "tinyauth/internal/docker" - "tinyauth/internal/hooks" - "tinyauth/internal/providers" - "tinyauth/internal/types" - - "github.com/gin-gonic/gin" -) - -type Handlers struct { - Config types.HandlersConfig - Auth *auth.Auth - Hooks *hooks.Hooks - Providers *providers.Providers - Docker *docker.Docker -} - -func NewHandlers(config types.HandlersConfig, auth *auth.Auth, hooks *hooks.Hooks, providers *providers.Providers, docker *docker.Docker) *Handlers { - return &Handlers{ - Config: config, - Auth: auth, - Hooks: hooks, - Providers: providers, - Docker: docker, - } -} - -func (h *Handlers) HealthcheckHandler(c *gin.Context) { - c.JSON(200, gin.H{ - "status": 200, - "message": "OK", - }) -} diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go deleted file mode 100644 index 279534d6..00000000 --- a/internal/handlers/handlers_test.go +++ /dev/null @@ -1,394 +0,0 @@ -package handlers_test - -import ( - "encoding/json" - "io" - "net/http" - "net/http/httptest" - "reflect" - "strings" - "testing" - "time" - "tinyauth/internal/auth" - "tinyauth/internal/docker" - "tinyauth/internal/handlers" - "tinyauth/internal/hooks" - "tinyauth/internal/providers" - "tinyauth/internal/server" - "tinyauth/internal/types" - - "github.com/magiconair/properties/assert" - "github.com/pquerna/otp/totp" -) - -// Simple server config -var serverConfig = types.ServerConfig{ - Port: 8080, - Address: "0.0.0.0", -} - -// Simple handlers config -var handlersConfig = types.HandlersConfig{ - AppURL: "http://localhost:8080", - Domain: "localhost", - DisableContinue: false, - CookieSecure: false, - Title: "Tinyauth", - GenericName: "Generic", - ForgotPasswordMessage: "Message", - CsrfCookieName: "tinyauth-csrf", - RedirectCookieName: "tinyauth-redirect", - BackgroundImage: "https://example.com/image.png", - OAuthAutoRedirect: "none", -} - -// Simple auth config -var authConfig = types.AuthConfig{ - Users: types.Users{}, - OauthWhitelist: "", - HMACSecret: "4bZ9K.*:;zH=,9zG!meUxu.B5-S[7.V.", // Complex on purpose - EncryptionSecret: "\\:!R(u[Sbv6ZLm.7es)H|OqH4y}0u\\rj", - CookieSecure: false, - SessionExpiry: 3600, - LoginTimeout: 0, - LoginMaxRetries: 0, - SessionCookieName: "tinyauth-session", - Domain: "localhost", -} - -// Simple hooks config -var hooksConfig = types.HooksConfig{ - Domain: "localhost", -} - -// Cookie -var cookie string - -// User -var user = types.User{ - Username: "user", - Password: "$2a$10$AvGHLTYv3xiRJ0xV9xs3XeVIlkGTygI9nqIamFYB5Xu.5.0UWF7B6", // pass -} - -// Initialize the server for tests -func getServer(t *testing.T) *server.Server { - // Create services - authConfig.Users = types.Users{ - { - Username: user.Username, - Password: user.Password, - TotpSecret: user.TotpSecret, - }, - } - docker, err := docker.NewDocker() - if err != nil { - t.Fatalf("Failed to create docker client: %v", err) - } - auth := auth.NewAuth(authConfig, nil, nil) - providers := providers.NewProviders(types.OAuthConfig{}) - hooks := hooks.NewHooks(hooksConfig, auth, providers) - handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker) - - // Create server - srv, err := server.NewServer(serverConfig, handlers) - if err != nil { - t.Fatalf("Failed to create server: %v", err) - } - - return srv -} - -func TestLogin(t *testing.T) { - t.Log("Testing login") - - srv := getServer(t) - - recorder := httptest.NewRecorder() - - user := types.LoginRequest{ - Username: "user", - Password: "pass", - } - - json, err := json.Marshal(user) - if err != nil { - t.Fatalf("Error marshalling json: %v", err) - } - - req, err := http.NewRequest("POST", "/api/login", strings.NewReader(string(json))) - if err != nil { - t.Fatalf("Error creating request: %v", err) - } - - srv.Router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusOK) - - cookies := recorder.Result().Cookies() - - if len(cookies) == 0 { - t.Fatalf("Cookie not set") - } - - // Set the cookie for further tests - cookie = cookies[0].Value -} - -func TestAppContext(t *testing.T) { - // Refresh the cookie - TestLogin(t) - - t.Log("Testing app context") - - srv := getServer(t) - - recorder := httptest.NewRecorder() - - req, err := http.NewRequest("GET", "/api/app", nil) - if err != nil { - t.Fatalf("Error creating request: %v", err) - } - - // Set the cookie from the previous test - req.AddCookie(&http.Cookie{ - Name: "tinyauth", - Value: cookie, - }) - - srv.Router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusOK) - - body, err := io.ReadAll(recorder.Body) - if err != nil { - t.Fatalf("Error getting body: %v", err) - } - - var app types.AppContext - - err = json.Unmarshal(body, &app) - if err != nil { - t.Fatalf("Error unmarshalling body: %v", err) - } - - expected := types.AppContext{ - Status: 200, - Message: "OK", - ConfiguredProviders: []string{"username"}, - DisableContinue: false, - Title: "Tinyauth", - GenericName: "Generic", - ForgotPasswordMessage: "Message", - BackgroundImage: "https://example.com/image.png", - OAuthAutoRedirect: "none", - Domain: "localhost", - } - - // We should get the username back - if !reflect.DeepEqual(app, expected) { - t.Fatalf("Expected %v, got %v", expected, app) - } -} - -func TestUserContext(t *testing.T) { - // Refresh the cookie - TestLogin(t) - - t.Log("Testing user context") - - srv := getServer(t) - - recorder := httptest.NewRecorder() - - req, err := http.NewRequest("GET", "/api/user", nil) - if err != nil { - t.Fatalf("Error creating request: %v", err) - } - - req.AddCookie(&http.Cookie{ - Name: "tinyauth-session", - Value: cookie, - }) - - srv.Router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusOK) - - body, err := io.ReadAll(recorder.Body) - if err != nil { - t.Fatalf("Error getting body: %v", err) - } - - type User struct { - Username string `json:"username"` - } - - var user User - - err = json.Unmarshal(body, &user) - if err != nil { - t.Fatalf("Error unmarshalling body: %v", err) - } - - // We should get the user back - if user.Username != "user" { - t.Fatalf("Expected user, got %s", user.Username) - } -} - -func TestLogout(t *testing.T) { - // Refresh the cookie - TestLogin(t) - - t.Log("Testing logout") - - srv := getServer(t) - - recorder := httptest.NewRecorder() - - req, err := http.NewRequest("POST", "/api/logout", nil) - if err != nil { - t.Fatalf("Error creating request: %v", err) - } - - req.AddCookie(&http.Cookie{ - Name: "tinyauth-session", - Value: cookie, - }) - - srv.Router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusOK) - - // Check if the cookie is different (means the cookie is gone) - if recorder.Result().Cookies()[0].Value == cookie { - t.Fatalf("Cookie not flushed") - } -} - -func TestAuth(t *testing.T) { - // Refresh the cookie - TestLogin(t) - - t.Log("Testing auth endpoint") - - srv := getServer(t) - - recorder := httptest.NewRecorder() - - req, err := http.NewRequest("GET", "/api/auth/traefik", nil) - if err != nil { - t.Fatalf("Error creating request: %v", err) - } - - req.Header.Set("Accept", "text/html") - - srv.Router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusTemporaryRedirect) - - recorder = httptest.NewRecorder() - - req, err = http.NewRequest("GET", "/api/auth/traefik", nil) - if err != nil { - t.Fatalf("Error creating request: %v", err) - } - - req.AddCookie(&http.Cookie{ - Name: "tinyauth-session", - Value: cookie, - }) - - srv.Router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusOK) - - recorder = httptest.NewRecorder() - - req, err = http.NewRequest("GET", "/api/auth/nginx", nil) - if err != nil { - t.Fatalf("Error creating request: %v", err) - } - - srv.Router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusUnauthorized) - - recorder = httptest.NewRecorder() - - req, err = http.NewRequest("GET", "/api/auth/nginx", nil) - if err != nil { - t.Fatalf("Error creating request: %v", err) - } - - req.AddCookie(&http.Cookie{ - Name: "tinyauth-session", - Value: cookie, - }) - - srv.Router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusOK) -} - -func TestTOTP(t *testing.T) { - t.Log("Testing TOTP") - - key, err := totp.Generate(totp.GenerateOpts{ - Issuer: "Tinyauth", - AccountName: user.Username, - }) - if err != nil { - t.Fatalf("Failed to generate TOTP secret: %v", err) - } - - secret := key.Secret() - - user.TotpSecret = secret - - srv := getServer(t) - - user := types.LoginRequest{ - Username: "user", - Password: "pass", - } - - loginJson, err := json.Marshal(user) - if err != nil { - t.Fatalf("Error marshalling json: %v", err) - } - - recorder := httptest.NewRecorder() - - req, err := http.NewRequest("POST", "/api/login", strings.NewReader(string(loginJson))) - if err != nil { - t.Fatalf("Error creating request: %v", err) - } - - srv.Router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusOK) - - // Set the cookie for next test - cookie = recorder.Result().Cookies()[0].Value - - code, err := totp.GenerateCode(secret, time.Now()) - if err != nil { - t.Fatalf("Failed to generate TOTP code: %v", err) - } - - totpRequest := types.TotpRequest{ - Code: code, - } - - totpJson, err := json.Marshal(totpRequest) - if err != nil { - t.Fatalf("Error marshalling TOTP request: %v", err) - } - - recorder = httptest.NewRecorder() - - req, err = http.NewRequest("POST", "/api/totp", strings.NewReader(string(totpJson))) - if err != nil { - t.Fatalf("Error creating request: %v", err) - } - - req.AddCookie(&http.Cookie{ - Name: "tinyauth-session", - Value: cookie, - }) - - srv.Router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusOK) -} diff --git a/internal/handlers/oauth.go b/internal/handlers/oauth.go deleted file mode 100644 index 13c3a474..00000000 --- a/internal/handlers/oauth.go +++ /dev/null @@ -1,223 +0,0 @@ -package handlers - -import ( - "fmt" - "net/http" - "strings" - "time" - "tinyauth/internal/types" - "tinyauth/internal/utils" - - "github.com/gin-gonic/gin" - "github.com/google/go-querystring/query" - "github.com/rs/zerolog/log" -) - -func (h *Handlers) OAuthURLHandler(c *gin.Context) { - var request types.OAuthRequest - - err := c.BindUri(&request) - if err != nil { - log.Error().Err(err).Msg("Failed to bind URI") - c.JSON(400, gin.H{ - "status": 400, - "message": "Bad Request", - }) - return - } - - log.Debug().Msg("Got OAuth request") - - // Check if provider exists - provider := h.Providers.GetProvider(request.Provider) - - if provider == nil { - c.JSON(404, gin.H{ - "status": 404, - "message": "Not Found", - }) - return - } - - log.Debug().Str("provider", request.Provider).Msg("Got provider") - - // Create state - state := provider.GenerateState() - - // Get auth URL - authURL := provider.GetAuthURL(state) - - log.Debug().Msg("Got auth URL") - - // Set CSRF cookie - c.SetCookie(h.Config.CsrfCookieName, state, int(time.Hour.Seconds()), "/", "", h.Config.CookieSecure, true) - - // Get redirect URI - redirectURI := c.Query("redirect_uri") - - // Set redirect cookie if redirect URI is provided - if redirectURI != "" { - log.Debug().Str("redirectURI", redirectURI).Msg("Setting redirect cookie") - c.SetCookie(h.Config.RedirectCookieName, redirectURI, int(time.Hour.Seconds()), "/", "", h.Config.CookieSecure, true) - } - - // Return auth URL - c.JSON(200, gin.H{ - "status": 200, - "message": "OK", - "url": authURL, - }) -} - -func (h *Handlers) OAuthCallbackHandler(c *gin.Context) { - var providerName types.OAuthRequest - - err := c.BindUri(&providerName) - if err != nil { - log.Error().Err(err).Msg("Failed to bind URI") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) - return - } - - log.Debug().Interface("provider", providerName.Provider).Msg("Got provider name") - - // Get state - state := c.Query("state") - - // Get CSRF cookie - csrfCookie, err := c.Cookie(h.Config.CsrfCookieName) - - if err != nil { - log.Debug().Msg("No CSRF cookie") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) - return - } - - log.Debug().Str("csrfCookie", csrfCookie).Msg("Got CSRF cookie") - - // Check if CSRF cookie is valid - if csrfCookie != state { - log.Warn().Msg("Invalid CSRF cookie or CSRF cookie does not match with the state") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) - return - } - - // Clean up CSRF cookie - c.SetCookie(h.Config.CsrfCookieName, "", -1, "/", "", h.Config.CookieSecure, true) - - // Get code - code := c.Query("code") - - log.Debug().Msg("Got code") - - // Get provider - provider := h.Providers.GetProvider(providerName.Provider) - - if provider == nil { - c.Redirect(http.StatusTemporaryRedirect, "/not-found") - return - } - - log.Debug().Str("provider", providerName.Provider).Msg("Got provider") - - // Exchange token (authenticates user) - _, err = provider.ExchangeToken(code) - if err != nil { - log.Error().Err(err).Msg("Failed to exchange token") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) - return - } - - log.Debug().Msg("Got token") - - // Get user - user, err := h.Providers.GetUser(providerName.Provider) - if err != nil { - log.Error().Err(err).Msg("Failed to get user") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) - return - } - - log.Debug().Interface("user", user).Msg("Got user") - - // Check that email is not empty - if user.Email == "" { - log.Error().Msg("Email is empty") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) - return - } - - // Email is not whitelisted - if !h.Auth.EmailWhitelisted(user.Email) { - log.Warn().Str("email", user.Email).Msg("Email not whitelisted") - queries, err := query.Values(types.UnauthorizedQuery{ - Username: user.Email, - }) - - if err != nil { - log.Error().Err(err).Msg("Failed to build queries") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) - return - } - - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", h.Config.AppURL, queries.Encode())) - } - - log.Debug().Msg("Email whitelisted") - - // Get username - var username string - - if user.PreferredUsername != "" { - username = user.PreferredUsername - } else { - username = fmt.Sprintf("%s_%s", strings.Split(user.Email, "@")[0], strings.Split(user.Email, "@")[1]) - } - - // Get name - var name string - - if user.Name != "" { - name = user.Name - } else { - name = fmt.Sprintf("%s (%s)", utils.Capitalize(strings.Split(user.Email, "@")[0]), strings.Split(user.Email, "@")[1]) - } - - // Create session cookie - h.Auth.CreateSessionCookie(c, &types.SessionCookie{ - Username: username, - Name: name, - Email: user.Email, - Provider: providerName.Provider, - OAuthGroups: utils.CoalesceToString(user.Groups), - }) - - // Check if we have a redirect URI - redirectCookie, err := c.Cookie(h.Config.RedirectCookieName) - - if err != nil { - log.Debug().Msg("No redirect cookie") - c.Redirect(http.StatusTemporaryRedirect, h.Config.AppURL) - return - } - - log.Debug().Str("redirectURI", redirectCookie).Msg("Got redirect URI") - - queries, err := query.Values(types.LoginQuery{ - RedirectURI: redirectCookie, - }) - - if err != nil { - log.Error().Err(err).Msg("Failed to build queries") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) - return - } - - log.Debug().Msg("Got redirect query") - - // Clean up redirect cookie - c.SetCookie(h.Config.RedirectCookieName, "", -1, "/", "", h.Config.CookieSecure, true) - - // Redirect to continue with the redirect URI - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/continue?%s", h.Config.AppURL, queries.Encode())) -} diff --git a/internal/handlers/proxy.go b/internal/handlers/proxy.go deleted file mode 100644 index fd87fd16..00000000 --- a/internal/handlers/proxy.go +++ /dev/null @@ -1,282 +0,0 @@ -package handlers - -import ( - "fmt" - "net/http" - "strings" - "tinyauth/internal/types" - "tinyauth/internal/utils" - - "github.com/gin-gonic/gin" - "github.com/google/go-querystring/query" - "github.com/rs/zerolog/log" -) - -func (h *Handlers) ProxyHandler(c *gin.Context) { - var proxy types.Proxy - - err := c.BindUri(&proxy) - if err != nil { - log.Error().Err(err).Msg("Failed to bind URI") - c.JSON(400, gin.H{ - "status": 400, - "message": "Bad Request", - }) - return - } - - // Check if the request is coming from a browser (tools like curl/bruno use */* and they don't include the text/html) - isBrowser := strings.Contains(c.Request.Header.Get("Accept"), "text/html") - - if isBrowser { - log.Debug().Msg("Request is most likely coming from a browser") - } else { - log.Debug().Msg("Request is most likely not coming from a browser") - } - - log.Debug().Interface("proxy", proxy.Proxy).Msg("Got proxy") - - uri := c.Request.Header.Get("X-Forwarded-Uri") - proto := c.Request.Header.Get("X-Forwarded-Proto") - host := c.Request.Header.Get("X-Forwarded-Host") - - hostPortless := strings.Split(host, ":")[0] // *lol* - id := strings.Split(hostPortless, ".")[0] - - labels, err := h.Docker.GetLabels(id, hostPortless) - if err != nil { - log.Error().Err(err).Msg("Failed to get container labels") - - if proxy.Proxy == "nginx" || !isBrowser { - c.JSON(500, gin.H{ - "status": 500, - "message": "Internal Server Error", - }) - return - } - - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) - return - } - - log.Debug().Interface("labels", labels).Msg("Got labels") - - ip := c.ClientIP() - - if h.Auth.BypassedIP(labels, ip) { - c.Header("Authorization", c.Request.Header.Get("Authorization")) - - headersParsed := utils.ParseHeaders(labels.Headers) - for key, value := range headersParsed { - log.Debug().Str("key", key).Msg("Setting header") - c.Header(key, value) - } - - if labels.Basic.Username != "" && utils.GetSecret(labels.Basic.Password.Plain, labels.Basic.Password.File) != "" { - log.Debug().Str("username", labels.Basic.Username).Msg("Setting basic auth headers") - c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(labels.Basic.Username, utils.GetSecret(labels.Basic.Password.Plain, labels.Basic.Password.File)))) - } - - c.JSON(200, gin.H{ - "status": 200, - "message": "Authenticated", - }) - return - } - - if !h.Auth.CheckIP(labels, ip) { - if proxy.Proxy == "nginx" || !isBrowser { - c.JSON(403, gin.H{ - "status": 403, - "message": "Forbidden", - }) - return - } - - values := types.UnauthorizedQuery{ - Resource: strings.Split(host, ".")[0], - IP: ip, - } - - queries, err := query.Values(values) - if err != nil { - log.Error().Err(err).Msg("Failed to build queries") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) - return - } - - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", h.Config.AppURL, queries.Encode())) - return - } - - authEnabled, err := h.Auth.AuthEnabled(uri, labels) - if err != nil { - log.Error().Err(err).Msg("Failed to check if app is allowed") - if proxy.Proxy == "nginx" || !isBrowser { - c.JSON(500, gin.H{ - "status": 500, - "message": "Internal Server Error", - }) - return - } - - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) - return - } - - if !authEnabled { - c.Header("Authorization", c.Request.Header.Get("Authorization")) - - headersParsed := utils.ParseHeaders(labels.Headers) - for key, value := range headersParsed { - log.Debug().Str("key", key).Msg("Setting header") - c.Header(key, value) - } - - if labels.Basic.Username != "" && utils.GetSecret(labels.Basic.Password.Plain, labels.Basic.Password.File) != "" { - log.Debug().Str("username", labels.Basic.Username).Msg("Setting basic auth headers") - c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(labels.Basic.Username, utils.GetSecret(labels.Basic.Password.Plain, labels.Basic.Password.File)))) - } - - c.JSON(200, gin.H{ - "status": 200, - "message": "Authenticated", - }) - - return - } - - userContext := h.Hooks.UseUserContext(c) - - // If we are using basic auth, we need to check if the user has totp and if it does then disable basic auth - if userContext.Provider == "basic" && userContext.TotpEnabled { - log.Warn().Str("username", userContext.Username).Msg("User has totp enabled, disabling basic auth") - userContext.IsLoggedIn = false - } - - if userContext.IsLoggedIn { - log.Debug().Msg("Authenticated") - - // Check if user is allowed to access subdomain, if request is nginx.example.com the subdomain (resource) is nginx - appAllowed := h.Auth.ResourceAllowed(c, userContext, labels) - - log.Debug().Bool("appAllowed", appAllowed).Msg("Checking if app is allowed") - - if !appAllowed { - log.Warn().Str("username", userContext.Username).Str("host", host).Msg("User not allowed") - - if proxy.Proxy == "nginx" || !isBrowser { - c.JSON(401, gin.H{ - "status": 401, - "message": "Unauthorized", - }) - return - } - - values := types.UnauthorizedQuery{ - Resource: strings.Split(host, ".")[0], - } - - if userContext.OAuth { - values.Username = userContext.Email - } else { - values.Username = userContext.Username - } - - queries, err := query.Values(values) - if err != nil { - log.Error().Err(err).Msg("Failed to build queries") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) - return - } - - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", h.Config.AppURL, queries.Encode())) - return - } - - if userContext.OAuth { - groupOk := h.Auth.OAuthGroup(c, userContext, labels) - - log.Debug().Bool("groupOk", groupOk).Msg("Checking if user is in required groups") - - if !groupOk { - log.Warn().Str("username", userContext.Username).Str("host", host).Msg("User is not in required groups") - if proxy.Proxy == "nginx" || !isBrowser { - c.JSON(401, gin.H{ - "status": 401, - "message": "Unauthorized", - }) - return - } - - values := types.UnauthorizedQuery{ - Resource: strings.Split(host, ".")[0], - GroupErr: true, - } - - if userContext.OAuth { - values.Username = userContext.Email - } else { - values.Username = userContext.Username - } - - queries, err := query.Values(values) - if err != nil { - log.Error().Err(err).Msg("Failed to build queries") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) - return - } - - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", h.Config.AppURL, queries.Encode())) - return - } - } - - c.Header("Authorization", c.Request.Header.Get("Authorization")) - c.Header("Remote-User", utils.SanitizeHeader(userContext.Username)) - c.Header("Remote-Name", utils.SanitizeHeader(userContext.Name)) - c.Header("Remote-Email", utils.SanitizeHeader(userContext.Email)) - c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups)) - - parsedHeaders := utils.ParseHeaders(labels.Headers) - for key, value := range parsedHeaders { - log.Debug().Str("key", key).Msg("Setting header") - c.Header(key, value) - } - - if labels.Basic.Username != "" && utils.GetSecret(labels.Basic.Password.Plain, labels.Basic.Password.File) != "" { - log.Debug().Str("username", labels.Basic.Username).Msg("Setting basic auth headers") - c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(labels.Basic.Username, utils.GetSecret(labels.Basic.Password.Plain, labels.Basic.Password.File)))) - } - - c.JSON(200, gin.H{ - "status": 200, - "message": "Authenticated", - }) - return - } - - // The user is not logged in - log.Debug().Msg("Unauthorized") - - if proxy.Proxy == "nginx" || !isBrowser { - c.JSON(401, gin.H{ - "status": 401, - "message": "Unauthorized", - }) - return - } - - queries, err := query.Values(types.LoginQuery{ - RedirectURI: fmt.Sprintf("%s://%s%s", proto, host, uri), - }) - - if err != nil { - log.Error().Err(err).Msg("Failed to build queries") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) - return - } - - log.Debug().Interface("redirect_uri", fmt.Sprintf("%s://%s%s", proto, host, uri)).Msg("Redirecting to login") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/login?%s", h.Config.AppURL, queries.Encode())) -} diff --git a/internal/handlers/user.go b/internal/handlers/user.go deleted file mode 100644 index 91d0fef5..00000000 --- a/internal/handlers/user.go +++ /dev/null @@ -1,197 +0,0 @@ -package handlers - -import ( - "fmt" - "strings" - "tinyauth/internal/types" - "tinyauth/internal/utils" - - "github.com/gin-gonic/gin" - "github.com/pquerna/otp/totp" - "github.com/rs/zerolog/log" -) - -func (h *Handlers) LoginHandler(c *gin.Context) { - var login types.LoginRequest - - err := c.BindJSON(&login) - if err != nil { - log.Error().Err(err).Msg("Failed to bind JSON") - c.JSON(400, gin.H{ - "status": 400, - "message": "Bad Request", - }) - return - } - - log.Debug().Msg("Got login request") - - clientIP := c.ClientIP() - - // Create an identifier for rate limiting (username or IP if username doesn't exist yet) - rateIdentifier := login.Username - if rateIdentifier == "" { - rateIdentifier = clientIP - } - - // Check if the account is locked due to too many failed attempts - locked, remainingTime := h.Auth.IsAccountLocked(rateIdentifier) - if locked { - log.Warn().Str("identifier", rateIdentifier).Int("remaining_seconds", remainingTime).Msg("Account is locked due to too many failed login attempts") - c.JSON(429, gin.H{ - "status": 429, - "message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remainingTime), - }) - return - } - - // Search for a user based on username - log.Debug().Interface("username", login.Username).Msg("Searching for user") - - userSearch := h.Auth.SearchUser(login.Username) - - // User does not exist - if userSearch.Type == "" { - log.Debug().Str("username", login.Username).Msg("User not found") - // Record failed login attempt - h.Auth.RecordLoginAttempt(rateIdentifier, false) - c.JSON(401, gin.H{ - "status": 401, - "message": "Unauthorized", - }) - return - } - - log.Debug().Msg("Got user") - - // Check if password is correct - if !h.Auth.VerifyUser(userSearch, login.Password) { - log.Debug().Str("username", login.Username).Msg("Password incorrect") - // Record failed login attempt - h.Auth.RecordLoginAttempt(rateIdentifier, false) - c.JSON(401, gin.H{ - "status": 401, - "message": "Unauthorized", - }) - return - } - - log.Debug().Msg("Password correct, checking totp") - - // Record successful login attempt (will reset failed attempt counter) - h.Auth.RecordLoginAttempt(rateIdentifier, true) - - // Check if user is using TOTP - if userSearch.Type == "local" { - // Get local user - localUser := h.Auth.GetLocalUser(login.Username) - - // Check if TOTP is enabled - if localUser.TotpSecret != "" { - log.Debug().Msg("Totp enabled") - - // Set totp pending cookie - h.Auth.CreateSessionCookie(c, &types.SessionCookie{ - Username: login.Username, - Name: utils.Capitalize(login.Username), - Email: fmt.Sprintf("%s@%s", strings.ToLower(login.Username), h.Config.Domain), - Provider: "username", - TotpPending: true, - }) - - // Return totp required - c.JSON(200, gin.H{ - "status": 200, - "message": "Waiting for totp", - "totpPending": true, - }) - return - } - } - - // Create session cookie with username as provider - h.Auth.CreateSessionCookie(c, &types.SessionCookie{ - Username: login.Username, - Name: utils.Capitalize(login.Username), - Email: fmt.Sprintf("%s@%s", strings.ToLower(login.Username), h.Config.Domain), - Provider: "username", - }) - - // Return logged in - c.JSON(200, gin.H{ - "status": 200, - "message": "Logged in", - "totpPending": false, - }) -} - -func (h *Handlers) TOTPHandler(c *gin.Context) { - var totpReq types.TotpRequest - - err := c.BindJSON(&totpReq) - if err != nil { - log.Error().Err(err).Msg("Failed to bind JSON") - c.JSON(400, gin.H{ - "status": 400, - "message": "Bad Request", - }) - return - } - - log.Debug().Msg("Checking totp") - - // Get user context - userContext := h.Hooks.UseUserContext(c) - - // Check if we have a user - if userContext.Username == "" { - log.Debug().Msg("No user context") - c.JSON(401, gin.H{ - "status": 401, - "message": "Unauthorized", - }) - return - } - - // Get user - user := h.Auth.GetLocalUser(userContext.Username) - - // Check if totp is correct - ok := totp.Validate(totpReq.Code, user.TotpSecret) - - if !ok { - log.Debug().Msg("Totp incorrect") - c.JSON(401, gin.H{ - "status": 401, - "message": "Unauthorized", - }) - return - } - - log.Debug().Msg("Totp correct") - - // Create session cookie with username as provider - h.Auth.CreateSessionCookie(c, &types.SessionCookie{ - Username: user.Username, - Name: utils.Capitalize(user.Username), - Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), h.Config.Domain), - Provider: "username", - }) - - // Return logged in - c.JSON(200, gin.H{ - "status": 200, - "message": "Logged in", - }) -} - -func (h *Handlers) LogoutHandler(c *gin.Context) { - log.Debug().Msg("Cleaning up redirect cookie") - - h.Auth.DeleteSessionCookie(c) - - c.JSON(200, gin.H{ - "status": 200, - "message": "Logged out", - }) -} diff --git a/internal/hooks/hooks.go b/internal/hooks/hooks.go deleted file mode 100644 index 3083b98e..00000000 --- a/internal/hooks/hooks.go +++ /dev/null @@ -1,144 +0,0 @@ -package hooks - -import ( - "fmt" - "strings" - "tinyauth/internal/auth" - "tinyauth/internal/oauth" - "tinyauth/internal/providers" - "tinyauth/internal/types" - "tinyauth/internal/utils" - - "github.com/gin-gonic/gin" - "github.com/rs/zerolog/log" -) - -type Hooks struct { - Config types.HooksConfig - Auth *auth.Auth - Providers *providers.Providers -} - -func NewHooks(config types.HooksConfig, auth *auth.Auth, providers *providers.Providers) *Hooks { - return &Hooks{ - Config: config, - Auth: auth, - Providers: providers, - } -} - -func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { - cookie, err := hooks.Auth.GetSessionCookie(c) - var provider *oauth.OAuth - - if err != nil { - log.Error().Err(err).Msg("Failed to get session cookie") - goto basic - } - - if cookie.TotpPending { - log.Debug().Msg("Totp pending") - return types.UserContext{ - Username: cookie.Username, - Name: cookie.Name, - Email: cookie.Email, - Provider: cookie.Provider, - TotpPending: true, - } - } - - if cookie.Provider == "username" { - log.Debug().Msg("Provider is username") - - userSearch := hooks.Auth.SearchUser(cookie.Username) - - if userSearch.Type == "unknown" { - log.Warn().Str("username", cookie.Username).Msg("User does not exist") - goto basic - } - - log.Debug().Str("type", userSearch.Type).Msg("User exists") - - return types.UserContext{ - Username: cookie.Username, - Name: cookie.Name, - Email: cookie.Email, - IsLoggedIn: true, - Provider: "username", - } - } - - log.Debug().Msg("Provider is not username") - - provider = hooks.Providers.GetProvider(cookie.Provider) - - if provider != nil { - log.Debug().Msg("Provider exists") - - if !hooks.Auth.EmailWhitelisted(cookie.Email) { - log.Warn().Str("email", cookie.Email).Msg("Email is not whitelisted") - hooks.Auth.DeleteSessionCookie(c) - goto basic - } - - log.Debug().Msg("Email is whitelisted") - - return types.UserContext{ - Username: cookie.Username, - Name: cookie.Name, - Email: cookie.Email, - IsLoggedIn: true, - OAuth: true, - Provider: cookie.Provider, - OAuthGroups: cookie.OAuthGroups, - } - } - -basic: - log.Debug().Msg("Trying basic auth") - - basic := hooks.Auth.GetBasicAuth(c) - - if basic != nil { - log.Debug().Msg("Got basic auth") - - userSearch := hooks.Auth.SearchUser(basic.Username) - - if userSearch.Type == "unkown" { - log.Error().Str("username", basic.Username).Msg("Basic auth user does not exist") - return types.UserContext{} - } - - if !hooks.Auth.VerifyUser(userSearch, basic.Password) { - log.Error().Str("username", basic.Username).Msg("Basic auth user password incorrect") - return types.UserContext{} - } - - if userSearch.Type == "ldap" { - log.Debug().Msg("User is LDAP") - - return types.UserContext{ - Username: basic.Username, - Name: utils.Capitalize(basic.Username), - Email: fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), hooks.Config.Domain), - IsLoggedIn: true, - Provider: "basic", - TotpEnabled: false, - } - } - - user := hooks.Auth.GetLocalUser(basic.Username) - - return types.UserContext{ - Username: basic.Username, - Name: utils.Capitalize(basic.Username), - Email: fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), hooks.Config.Domain), - IsLoggedIn: true, - Provider: "basic", - TotpEnabled: user.TotpSecret != "", - } - - } - - return types.UserContext{} -} diff --git a/internal/middleware/context_middleware.go b/internal/middleware/context_middleware.go new file mode 100644 index 00000000..58e53e15 --- /dev/null +++ b/internal/middleware/context_middleware.go @@ -0,0 +1,159 @@ +package middleware + +import ( + "fmt" + "strings" + "tinyauth/internal/config" + "tinyauth/internal/service" + "tinyauth/internal/utils" + + "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" +) + +type ContextMiddlewareConfig struct { + Domain string +} + +type ContextMiddleware struct { + Config ContextMiddlewareConfig + Auth *service.AuthService + Broker *service.OAuthBrokerService +} + +func NewContextMiddleware(config ContextMiddlewareConfig, auth *service.AuthService, broker *service.OAuthBrokerService) *ContextMiddleware { + return &ContextMiddleware{ + Config: config, + Auth: auth, + Broker: broker, + } +} + +func (m *ContextMiddleware) Init() error { + return nil +} + +func (m *ContextMiddleware) Middleware() gin.HandlerFunc { + return func(c *gin.Context) { + cookie, err := m.Auth.GetSessionCookie(c) + + if err != nil { + log.Debug().Err(err).Msg("No valid session cookie found") + goto basic + } + + if cookie.TotpPending { + c.Set("context", &config.UserContext{ + Username: cookie.Username, + Name: cookie.Name, + Email: cookie.Email, + Provider: "username", + TotpPending: true, + TotpEnabled: true, + }) + c.Next() + return + } + + switch cookie.Provider { + case "username": + userSearch := m.Auth.SearchUser(cookie.Username) + + if userSearch.Type == "unknown" { + log.Debug().Msg("User from session cookie not found") + m.Auth.DeleteSessionCookie(c) + goto basic + } + + c.Set("context", &config.UserContext{ + Username: cookie.Username, + Name: cookie.Name, + Email: cookie.Email, + Provider: "username", + IsLoggedIn: true, + }) + c.Next() + return + default: + _, exists := m.Broker.GetService(cookie.Provider) + + if !exists { + log.Debug().Msg("OAuth provider from session cookie not found") + m.Auth.DeleteSessionCookie(c) + goto basic + } + + if !m.Auth.EmailWhitelisted(cookie.Email) { + log.Debug().Msg("Email from session cookie not whitelisted") + m.Auth.DeleteSessionCookie(c) + goto basic + } + + c.Set("context", &config.UserContext{ + Username: cookie.Username, + Name: cookie.Name, + Email: cookie.Email, + Provider: cookie.Provider, + OAuthGroups: cookie.OAuthGroups, + IsLoggedIn: true, + OAuth: true, + }) + c.Next() + return + } + + basic: + basic := m.Auth.GetBasicAuth(c) + + if basic == nil { + log.Debug().Msg("No basic auth provided") + c.Next() + return + } + + userSearch := m.Auth.SearchUser(basic.Username) + + if userSearch.Type == "unknown" { + log.Debug().Msg("User from basic auth not found") + c.Next() + return + } + + if !m.Auth.VerifyUser(userSearch, basic.Password) { + log.Debug().Msg("Invalid password for basic auth user") + c.Next() + return + } + + switch userSearch.Type { + case "local": + log.Debug().Msg("Basic auth user is local") + + user := m.Auth.GetLocalUser(basic.Username) + + c.Set("context", &config.UserContext{ + Username: user.Username, + Name: utils.Capitalize(user.Username), + Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), m.Config.Domain), + Provider: "basic", + IsLoggedIn: true, + TotpEnabled: user.TotpSecret != "", + }) + c.Next() + return + case "ldap": + log.Debug().Msg("Basic auth user is LDAP") + c.Set("context", &config.UserContext{ + Username: basic.Username, + Name: utils.Capitalize(basic.Username), + Email: fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), m.Config.Domain), + Provider: "basic", + IsLoggedIn: true, + }) + c.Next() + return + } + + c.Next() + } +} diff --git a/internal/middleware/ui_middleware.go b/internal/middleware/ui_middleware.go new file mode 100644 index 00000000..dcfaa35b --- /dev/null +++ b/internal/middleware/ui_middleware.go @@ -0,0 +1,56 @@ +package middleware + +import ( + "io/fs" + "net/http" + "os" + "strings" + "tinyauth/internal/assets" + + "github.com/gin-gonic/gin" +) + +type UIMiddleware struct { + UIFS fs.FS + UIFileServer http.Handler +} + +func NewUIMiddleware() *UIMiddleware { + return &UIMiddleware{} +} + +func (m *UIMiddleware) Init() error { + ui, err := fs.Sub(assets.FrontendAssets, "dist") + + if err != nil { + return err + } + + m.UIFS = ui + m.UIFileServer = http.FileServer(http.FS(ui)) + + return nil +} + +func (m *UIMiddleware) Middleware() gin.HandlerFunc { + return func(c *gin.Context) { + switch strings.Split(c.Request.URL.Path, "/")[1] { + case "api": + c.Next() + return + case "resources": + c.Next() + return + default: + _, err := fs.Stat(m.UIFS, strings.TrimPrefix(c.Request.URL.Path, "/")) + + if os.IsNotExist(err) { + c.Request.URL.Path = "/" + } + + m.UIFileServer.ServeHTTP(c.Writer, c.Request) + c.Abort() + return + } + } +} diff --git a/internal/middleware/zerolog_middleware.go b/internal/middleware/zerolog_middleware.go new file mode 100644 index 00000000..877ad4c8 --- /dev/null +++ b/internal/middleware/zerolog_middleware.go @@ -0,0 +1,66 @@ +package middleware + +import ( + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" +) + +var ( + loggerSkipPathsPrefix = []string{ + "GET /api/health", + "HEAD /api/health", + "GET /favicon.ico", + } +) + +type ZerologMiddleware struct{} + +func NewZerologMiddleware() *ZerologMiddleware { + return &ZerologMiddleware{} +} + +func (m *ZerologMiddleware) Init() error { + return nil +} + +func (m *ZerologMiddleware) logPath(path string) bool { + for _, prefix := range loggerSkipPathsPrefix { + if strings.HasPrefix(path, prefix) { + return false + } + } + return true +} + +func (m *ZerologMiddleware) Middleware() gin.HandlerFunc { + return func(c *gin.Context) { + tStart := time.Now() + + c.Next() + + code := c.Writer.Status() + address := c.Request.RemoteAddr + clientIP := c.ClientIP() + method := c.Request.Method + path := c.Request.URL.Path + + latency := time.Since(tStart).String() + + // logPath check if the path should be logged normally or with debug + if m.logPath(method + " " + path) { + switch { + case code >= 200 && code < 300: + log.Info().Str("method", method).Str("path", path).Str("address", address).Str("clientIp", clientIP).Int("status", code).Str("latency", latency).Msg("Request") + case code >= 300 && code < 400: + log.Warn().Str("method", method).Str("path", path).Str("address", address).Str("clientIp", clientIP).Int("status", code).Str("latency", latency).Msg("Request") + case code >= 400: + log.Error().Str("method", method).Str("path", path).Str("address", address).Str("clientIp", clientIP).Int("status", code).Str("latency", latency).Msg("Request") + } + } else { + log.Debug().Str("method", method).Str("path", path).Str("address", address).Int("status", code).Str("latency", latency).Msg("Request") + } + } +} diff --git a/internal/oauth/oauth.go b/internal/oauth/oauth.go deleted file mode 100644 index 9529fce5..00000000 --- a/internal/oauth/oauth.go +++ /dev/null @@ -1,71 +0,0 @@ -package oauth - -import ( - "context" - "crypto/rand" - "crypto/tls" - "encoding/base64" - "net/http" - - "golang.org/x/oauth2" -) - -type OAuth struct { - Config oauth2.Config - Context context.Context - Token *oauth2.Token - Verifier string -} - -func NewOAuth(config oauth2.Config, insecureSkipVerify bool) *OAuth { - transport := &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: insecureSkipVerify, - MinVersion: tls.VersionTLS12, - }, - } - - httpClient := &http.Client{ - Transport: transport, - } - - ctx := context.Background() - - // Set the HTTP client in the context - ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) - - verifier := oauth2.GenerateVerifier() - - return &OAuth{ - Config: config, - Context: ctx, - Verifier: verifier, - } -} - -func (oauth *OAuth) GetAuthURL(state string) string { - return oauth.Config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(oauth.Verifier)) -} - -func (oauth *OAuth) ExchangeToken(code string) (string, error) { - token, err := oauth.Config.Exchange(oauth.Context, code, oauth2.VerifierOption(oauth.Verifier)) - - if err != nil { - return "", err - } - - // Set and return the token - oauth.Token = token - return oauth.Token.AccessToken, nil -} - -func (oauth *OAuth) GetClient() *http.Client { - return oauth.Config.Client(oauth.Context, oauth.Token) -} - -func (oauth *OAuth) GenerateState() string { - b := make([]byte, 128) - rand.Read(b) - state := base64.URLEncoding.EncodeToString(b) - return state -} diff --git a/internal/providers/generic.go b/internal/providers/generic.go deleted file mode 100644 index 200f7c4b..00000000 --- a/internal/providers/generic.go +++ /dev/null @@ -1,37 +0,0 @@ -package providers - -import ( - "encoding/json" - "io" - "net/http" - "tinyauth/internal/constants" - - "github.com/rs/zerolog/log" -) - -func GetGenericUser(client *http.Client, url string) (constants.Claims, error) { - var user constants.Claims - - res, err := client.Get(url) - if err != nil { - return user, err - } - defer res.Body.Close() - - log.Debug().Msg("Got response from generic provider") - - body, err := io.ReadAll(res.Body) - if err != nil { - return user, err - } - - log.Debug().Msg("Read body from generic provider") - - err = json.Unmarshal(body, &user) - if err != nil { - return user, err - } - - log.Debug().Msg("Parsed user from generic provider") - return user, nil -} diff --git a/internal/providers/github.go b/internal/providers/github.go deleted file mode 100644 index 67f85104..00000000 --- a/internal/providers/github.go +++ /dev/null @@ -1,102 +0,0 @@ -package providers - -import ( - "encoding/json" - "errors" - "io" - "net/http" - "tinyauth/internal/constants" - - "github.com/rs/zerolog/log" -) - -// Response for the github email endpoint -type GithubEmailResponse []struct { - Email string `json:"email"` - Primary bool `json:"primary"` -} - -// Response for the github user endpoint -type GithubUserInfoResponse struct { - Login string `json:"login"` - Name string `json:"name"` -} - -// The scopes required for the github provider -func GithubScopes() []string { - return []string{"user:email", "read:user"} -} - -func GetGithubUser(client *http.Client) (constants.Claims, error) { - var user constants.Claims - - res, err := client.Get("https://api.github.com/user") - if err != nil { - return user, err - } - defer res.Body.Close() - - log.Debug().Msg("Got user response from github") - - body, err := io.ReadAll(res.Body) - if err != nil { - return user, err - } - - log.Debug().Msg("Read user body from github") - - var userInfo GithubUserInfoResponse - - err = json.Unmarshal(body, &userInfo) - if err != nil { - return user, err - } - - res, err = client.Get("https://api.github.com/user/emails") - if err != nil { - return user, err - } - defer res.Body.Close() - - log.Debug().Msg("Got email response from github") - - body, err = io.ReadAll(res.Body) - if err != nil { - return user, err - } - - log.Debug().Msg("Read email body from github") - - var emails GithubEmailResponse - - err = json.Unmarshal(body, &emails) - if err != nil { - return user, err - } - - log.Debug().Msg("Parsed emails from github") - - // Find and return the primary email - for _, email := range emails { - if email.Primary { - log.Debug().Str("email", email.Email).Msg("Found primary email") - user.Email = email.Email - break - } - } - - if len(emails) == 0 { - return user, errors.New("no emails found") - } - - // Use first available email if no primary email was found - if user.Email == "" { - log.Warn().Str("email", emails[0].Email).Msg("No primary email found, using first email") - user.Email = emails[0].Email - } - - user.PreferredUsername = userInfo.Login - user.Name = userInfo.Name - - return user, nil -} diff --git a/internal/providers/google.go b/internal/providers/google.go deleted file mode 100644 index e794beec..00000000 --- a/internal/providers/google.go +++ /dev/null @@ -1,56 +0,0 @@ -package providers - -import ( - "encoding/json" - "io" - "net/http" - "strings" - "tinyauth/internal/constants" - - "github.com/rs/zerolog/log" -) - -// Response for the google user endpoint -type GoogleUserInfoResponse struct { - Email string `json:"email"` - Name string `json:"name"` -} - -// The scopes required for the google provider -func GoogleScopes() []string { - return []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"} -} - -func GetGoogleUser(client *http.Client) (constants.Claims, error) { - var user constants.Claims - - res, err := client.Get("https://www.googleapis.com/userinfo/v2/me") - if err != nil { - return user, err - } - defer res.Body.Close() - - log.Debug().Msg("Got response from google") - - body, err := io.ReadAll(res.Body) - if err != nil { - return user, err - } - - log.Debug().Msg("Read body from google") - - var userInfo GoogleUserInfoResponse - - err = json.Unmarshal(body, &userInfo) - if err != nil { - return user, err - } - - log.Debug().Msg("Parsed user from google") - - user.PreferredUsername = strings.Split(userInfo.Email, "@")[0] - user.Name = userInfo.Name - user.Email = userInfo.Email - - return user, nil -} diff --git a/internal/providers/providers.go b/internal/providers/providers.go deleted file mode 100644 index 7af127ea..00000000 --- a/internal/providers/providers.go +++ /dev/null @@ -1,154 +0,0 @@ -package providers - -import ( - "fmt" - "tinyauth/internal/constants" - "tinyauth/internal/oauth" - "tinyauth/internal/types" - - "github.com/rs/zerolog/log" - "golang.org/x/oauth2" - "golang.org/x/oauth2/endpoints" -) - -type Providers struct { - Config types.OAuthConfig - Github *oauth.OAuth - Google *oauth.OAuth - Generic *oauth.OAuth -} - -func NewProviders(config types.OAuthConfig) *Providers { - providers := &Providers{ - Config: config, - } - - if config.GithubClientId != "" && config.GithubClientSecret != "" { - log.Info().Msg("Initializing Github OAuth") - providers.Github = oauth.NewOAuth(oauth2.Config{ - ClientID: config.GithubClientId, - ClientSecret: config.GithubClientSecret, - RedirectURL: fmt.Sprintf("%s/api/oauth/callback/github", config.AppURL), - Scopes: GithubScopes(), - Endpoint: endpoints.GitHub, - }, false) - } - - if config.GoogleClientId != "" && config.GoogleClientSecret != "" { - log.Info().Msg("Initializing Google OAuth") - providers.Google = oauth.NewOAuth(oauth2.Config{ - ClientID: config.GoogleClientId, - ClientSecret: config.GoogleClientSecret, - RedirectURL: fmt.Sprintf("%s/api/oauth/callback/google", config.AppURL), - Scopes: GoogleScopes(), - Endpoint: endpoints.Google, - }, false) - } - - if config.GenericClientId != "" && config.GenericClientSecret != "" { - log.Info().Msg("Initializing Generic OAuth") - providers.Generic = oauth.NewOAuth(oauth2.Config{ - ClientID: config.GenericClientId, - ClientSecret: config.GenericClientSecret, - RedirectURL: fmt.Sprintf("%s/api/oauth/callback/generic", config.AppURL), - Scopes: config.GenericScopes, - Endpoint: oauth2.Endpoint{ - AuthURL: config.GenericAuthURL, - TokenURL: config.GenericTokenURL, - }, - }, config.GenericSkipSSL) - } - - return providers -} - -func (providers *Providers) GetProvider(provider string) *oauth.OAuth { - switch provider { - case "github": - return providers.Github - case "google": - return providers.Google - case "generic": - return providers.Generic - default: - return nil - } -} - -func (providers *Providers) GetUser(provider string) (constants.Claims, error) { - var user constants.Claims - - // Get the user from the provider - switch provider { - case "github": - if providers.Github == nil { - log.Debug().Msg("Github provider not configured") - return user, nil - } - - client := providers.Github.GetClient() - - log.Debug().Msg("Got client from github") - - user, err := GetGithubUser(client) - if err != nil { - return user, err - } - - log.Debug().Msg("Got user from github") - - return user, nil - case "google": - if providers.Google == nil { - log.Debug().Msg("Google provider not configured") - return user, nil - } - - client := providers.Google.GetClient() - - log.Debug().Msg("Got client from google") - - user, err := GetGoogleUser(client) - if err != nil { - return user, err - } - - log.Debug().Msg("Got user from google") - - return user, nil - case "generic": - if providers.Generic == nil { - log.Debug().Msg("Generic provider not configured") - return user, nil - } - - client := providers.Generic.GetClient() - - log.Debug().Msg("Got client from generic") - - user, err := GetGenericUser(client, providers.Config.GenericUserURL) - if err != nil { - return user, err - } - - log.Debug().Msg("Got user from generic") - - return user, nil - default: - return user, nil - } -} - -func (provider *Providers) GetConfiguredProviders() []string { - providers := []string{} - if provider.Github != nil { - providers = append(providers, "github") - } - if provider.Google != nil { - providers = append(providers, "google") - } - if provider.Generic != nil { - providers = append(providers, "generic") - } - return providers -} diff --git a/internal/server/server.go b/internal/server/server.go deleted file mode 100644 index 88260322..00000000 --- a/internal/server/server.go +++ /dev/null @@ -1,130 +0,0 @@ -package server - -import ( - "fmt" - "io/fs" - "net/http" - "os" - "strings" - "time" - "tinyauth/internal/assets" - "tinyauth/internal/handlers" - "tinyauth/internal/types" - - "github.com/gin-gonic/gin" - "github.com/rs/zerolog/log" -) - -type Server struct { - Config types.ServerConfig - Handlers *handlers.Handlers - Router *gin.Engine -} - -var ( - loggerSkipPathsPrefix = []string{ - "GET /api/healthcheck", - "HEAD /api/healthcheck", - "GET /favicon.ico", - } -) - -func logPath(path string) bool { - for _, prefix := range loggerSkipPathsPrefix { - if strings.HasPrefix(path, prefix) { - return false - } - } - return true -} - -func NewServer(config types.ServerConfig, handlers *handlers.Handlers) (*Server, error) { - gin.SetMode(gin.ReleaseMode) - - log.Debug().Msg("Setting up router") - router := gin.New() - router.Use(zerolog()) - - log.Debug().Msg("Setting up assets") - dist, err := fs.Sub(assets.Assets, "dist") - if err != nil { - return nil, err - } - - log.Debug().Msg("Setting up file server") - fileServer := http.FileServer(http.FS(dist)) - - // UI middleware - router.Use(func(c *gin.Context) { - // If not an API request, serve the UI - if !strings.HasPrefix(c.Request.URL.Path, "/api") { - _, err := fs.Stat(dist, strings.TrimPrefix(c.Request.URL.Path, "/")) - if os.IsNotExist(err) { - c.Request.URL.Path = "/" - } - fileServer.ServeHTTP(c.Writer, c.Request) - c.Abort() - } - }) - - // Proxy routes - router.GET("/api/auth/:proxy", handlers.ProxyHandler) - - // Auth routes - router.POST("/api/login", handlers.LoginHandler) - router.POST("/api/totp", handlers.TOTPHandler) - router.POST("/api/logout", handlers.LogoutHandler) - - // Context routes - router.GET("/api/app", handlers.AppContextHandler) - router.GET("/api/user", handlers.UserContextHandler) - - // OAuth routes - router.GET("/api/oauth/url/:provider", handlers.OAuthURLHandler) - router.GET("/api/oauth/callback/:provider", handlers.OAuthCallbackHandler) - - // App routes - router.GET("/api/healthcheck", handlers.HealthcheckHandler) - router.HEAD("/api/healthcheck", handlers.HealthcheckHandler) - - return &Server{ - Config: config, - Handlers: handlers, - Router: router, - }, nil -} - -func (s *Server) Start() error { - log.Info().Str("address", s.Config.Address).Int("port", s.Config.Port).Msg("Starting server") - return s.Router.Run(fmt.Sprintf("%s:%d", s.Config.Address, s.Config.Port)) -} - -// zerolog is a middleware for gin that logs requests using zerolog -func zerolog() gin.HandlerFunc { - return func(c *gin.Context) { - tStart := time.Now() - - c.Next() - - code := c.Writer.Status() - address := c.Request.RemoteAddr - method := c.Request.Method - path := c.Request.URL.Path - - latency := time.Since(tStart).String() - - // logPath check if the path should be logged normally or with debug - if logPath(method + " " + path) { - switch { - case code >= 200 && code < 300: - log.Info().Str("method", method).Str("path", path).Str("address", address).Int("status", code).Str("latency", latency).Msg("Request") - case code >= 300 && code < 400: - log.Warn().Str("method", method).Str("path", path).Str("address", address).Int("status", code).Str("latency", latency).Msg("Request") - case code >= 400: - log.Error().Str("method", method).Str("path", path).Str("address", address).Int("status", code).Str("latency", latency).Msg("Request") - } - } else { - log.Debug().Str("method", method).Str("path", path).Str("address", address).Int("status", code).Str("latency", latency).Msg("Request") - } - } -} diff --git a/internal/auth/auth.go b/internal/service/auth_service.go similarity index 62% rename from internal/auth/auth.go rename to internal/service/auth_service.go index 3f18419a..10d49e79 100644 --- a/internal/auth/auth.go +++ b/internal/service/auth_service.go @@ -1,4 +1,4 @@ -package auth +package service import ( "fmt" @@ -6,9 +6,7 @@ import ( "strings" "sync" "time" - "tinyauth/internal/docker" - "tinyauth/internal/ldap" - "tinyauth/internal/types" + "tinyauth/internal/config" "tinyauth/internal/utils" "github.com/gin-gonic/gin" @@ -17,44 +15,66 @@ import ( "golang.org/x/crypto/bcrypt" ) -type Auth struct { - Config types.AuthConfig - Docker *docker.Docker - LoginAttempts map[string]*types.LoginAttempt +type LoginAttempt struct { + FailedAttempts int + LastAttempt time.Time + LockedUntil time.Time +} + +type AuthServiceConfig struct { + Users []config.User + OauthWhitelist string + SessionExpiry int + SecureCookie bool + Domain string + LoginTimeout int + LoginMaxRetries int + SessionCookieName string + HMACSecret string + EncryptionSecret string +} + +type AuthService struct { + Config AuthServiceConfig + Docker *DockerService + LoginAttempts map[string]*LoginAttempt LoginMutex sync.RWMutex Store *sessions.CookieStore - LDAP *ldap.LDAP + LDAP *LdapService } -func NewAuth(config types.AuthConfig, docker *docker.Docker, ldap *ldap.LDAP) *Auth { - // Setup cookie store and create the auth service - store := sessions.NewCookieStore([]byte(config.HMACSecret), []byte(config.EncryptionSecret)) - store.Options = &sessions.Options{ - Path: "/", - MaxAge: config.SessionExpiry, - Secure: config.CookieSecure, - HttpOnly: true, - Domain: fmt.Sprintf(".%s", config.Domain), - } - return &Auth{ +func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService) *AuthService { + return &AuthService{ Config: config, Docker: docker, - LoginAttempts: make(map[string]*types.LoginAttempt), - Store: store, + LoginAttempts: make(map[string]*LoginAttempt), LDAP: ldap, } } -func (auth *Auth) GetSession(c *gin.Context) (*sessions.Session, error) { +func (auth *AuthService) Init() error { + store := sessions.NewCookieStore([]byte(auth.Config.HMACSecret), []byte(auth.Config.EncryptionSecret)) + store.Options = &sessions.Options{ + Path: "/", + MaxAge: auth.Config.SessionExpiry, + Secure: auth.Config.SecureCookie, + HttpOnly: true, + Domain: fmt.Sprintf(".%s", auth.Config.Domain), + } + + auth.Store = store + return nil +} + +func (auth *AuthService) GetSession(c *gin.Context) (*sessions.Session, error) { session, err := auth.Store.Get(c.Request, auth.Config.SessionCookieName) // If there was an error getting the session, it might be invalid so let's clear it and retry if err != nil { - log.Error().Err(err).Msg("Invalid session, clearing cookie and retrying") - c.SetCookie(auth.Config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.Config.Domain), auth.Config.CookieSecure, true) - session, err = auth.Store.Get(c.Request, auth.Config.SessionCookieName) + log.Debug().Err(err).Msg("Error getting session, creating a new one") + c.SetCookie(auth.Config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.Config.Domain), auth.Config.SecureCookie, true) + session, err = auth.Store.New(c.Request, auth.Config.SessionCookieName) if err != nil { - log.Error().Err(err).Msg("Failed to get session") return nil, err } } @@ -62,95 +82,79 @@ func (auth *Auth) GetSession(c *gin.Context) (*sessions.Session, error) { return session, nil } -func (auth *Auth) SearchUser(username string) types.UserSearch { - log.Debug().Str("username", username).Msg("Searching for user") - - // Check local users first +func (auth *AuthService) SearchUser(username string) config.UserSearch { if auth.GetLocalUser(username).Username != "" { - log.Debug().Str("username", username).Msg("Found local user") - return types.UserSearch{ + return config.UserSearch{ Username: username, Type: "local", } } - // If no user found, check LDAP if auth.LDAP != nil { - log.Debug().Str("username", username).Msg("Checking LDAP for user") userDN, err := auth.LDAP.Search(username) + if err != nil { - log.Warn().Err(err).Str("username", username).Msg("Failed to find user in LDAP") - return types.UserSearch{} + log.Warn().Err(err).Str("username", username).Msg("Failed to search for user in LDAP") + return config.UserSearch{} } - return types.UserSearch{ + + return config.UserSearch{ Username: userDN, Type: "ldap", } } - return types.UserSearch{ + return config.UserSearch{ Type: "unknown", } } -func (auth *Auth) VerifyUser(search types.UserSearch, password string) bool { - // Authenticate the user based on the type +func (auth *AuthService) VerifyUser(search config.UserSearch, password string) bool { switch search.Type { case "local": - // If local user, get the user and check the password user := auth.GetLocalUser(search.Username) return auth.CheckPassword(user, password) case "ldap": - // If LDAP is configured, bind to the LDAP server with the user DN and password if auth.LDAP != nil { - log.Debug().Str("username", search.Username).Msg("Binding to LDAP for user authentication") - err := auth.LDAP.Bind(search.Username, password) if err != nil { log.Warn().Err(err).Str("username", search.Username).Msg("Failed to bind to LDAP") return false } - // Rebind with the service account to reset the connection err = auth.LDAP.Bind(auth.LDAP.Config.BindDN, auth.LDAP.Config.BindPassword) if err != nil { log.Error().Err(err).Msg("Failed to rebind with service account after user authentication") return false } - log.Debug().Str("username", search.Username).Msg("LDAP authentication successful") return true } default: - log.Warn().Str("type", search.Type).Msg("Unknown user type for authentication") + log.Debug().Str("type", search.Type).Msg("Unknown user type for authentication") return false } - // If no user found or authentication failed, return false log.Warn().Str("username", search.Username).Msg("User authentication failed") return false } -func (auth *Auth) GetLocalUser(username string) types.User { - // Loop through users and return the user if the username matches - log.Debug().Str("username", username).Msg("Searching for local user") - +func (auth *AuthService) GetLocalUser(username string) config.User { for _, user := range auth.Config.Users { if user.Username == username { return user } } - // If no user found, return an empty user log.Warn().Str("username", username).Msg("Local user not found") - return types.User{} + return config.User{} } -func (auth *Auth) CheckPassword(user types.User, password string) bool { +func (auth *AuthService) CheckPassword(user config.User, password string) bool { return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil } -func (auth *Auth) IsAccountLocked(identifier string) (bool, int) { +func (auth *AuthService) IsAccountLocked(identifier string) (bool, int) { auth.LoginMutex.RLock() defer auth.LoginMutex.RUnlock() @@ -176,7 +180,7 @@ func (auth *Auth) IsAccountLocked(identifier string) (bool, int) { return false, 0 } -func (auth *Auth) RecordLoginAttempt(identifier string, success bool) { +func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) { // Skip if rate limiting is not configured if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 { return @@ -188,7 +192,7 @@ func (auth *Auth) RecordLoginAttempt(identifier string, success bool) { // Get current attempt record or create a new one attempt, exists := auth.LoginAttempts[identifier] if !exists { - attempt = &types.LoginAttempt{} + attempt = &LoginAttempt{} auth.LoginAttempts[identifier] = attempt } @@ -212,21 +216,16 @@ func (auth *Auth) RecordLoginAttempt(identifier string, success bool) { } } -func (auth *Auth) EmailWhitelisted(email string) bool { +func (auth *AuthService) EmailWhitelisted(email string) bool { return utils.CheckFilter(auth.Config.OauthWhitelist, email) } -func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) error { - log.Debug().Msg("Creating session cookie") - +func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.SessionCookie) error { session, err := auth.GetSession(c) if err != nil { - log.Error().Err(err).Msg("Failed to get session") return err } - log.Debug().Msg("Setting session cookie") - var sessionExpiry int if data.TotpPending { @@ -245,19 +244,15 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) err = session.Save(c.Request, c.Writer) if err != nil { - log.Error().Err(err).Msg("Failed to save session") return err } return nil } -func (auth *Auth) DeleteSessionCookie(c *gin.Context) error { - log.Debug().Msg("Deleting session cookie") - +func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error { session, err := auth.GetSession(c) if err != nil { - log.Error().Err(err).Msg("Failed to get session") return err } @@ -268,24 +263,21 @@ func (auth *Auth) DeleteSessionCookie(c *gin.Context) error { err = session.Save(c.Request, c.Writer) if err != nil { - log.Error().Err(err).Msg("Failed to save session") return err } + // Clear the cookie in the browser + c.SetCookie(auth.Config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.Config.Domain), auth.Config.SecureCookie, true) + return nil } -func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) { - log.Debug().Msg("Getting session cookie") - +func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie, error) { session, err := auth.GetSession(c) if err != nil { - log.Error().Err(err).Msg("Failed to get session") - return types.SessionCookie{}, err + return config.SessionCookie{}, err } - log.Debug().Msg("Got session") - username, usernameOk := session.Values["username"].(string) email, emailOk := session.Values["email"].(string) name, nameOk := session.Values["name"].(string) @@ -298,18 +290,17 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) if !usernameOk || !providerOK || !expiryOk || !totpPendingOk || !emailOk || !nameOk || !oauthGroupsOk { log.Warn().Msg("Session cookie is invalid") auth.DeleteSessionCookie(c) - return types.SessionCookie{}, nil + return config.SessionCookie{}, nil } // If the session cookie has expired, delete it if time.Now().Unix() > expiry { log.Warn().Msg("Session cookie expired") auth.DeleteSessionCookie(c) - return types.SessionCookie{}, nil + return config.SessionCookie{}, nil } - log.Debug().Str("username", username).Str("provider", provider).Int64("expiry", expiry).Bool("totpPending", totpPending).Str("name", name).Str("email", email).Str("oauthGroups", oauthGroups).Msg("Parsed cookie") - return types.SessionCookie{ + return config.SessionCookie{ Username: username, Name: name, Email: email, @@ -319,12 +310,12 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) }, nil } -func (auth *Auth) UserAuthConfigured() bool { +func (auth *AuthService) UserAuthConfigured() bool { // If there are users or LDAP is configured, return true return len(auth.Config.Users) > 0 || auth.LDAP != nil } -func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext, labels types.Labels) bool { +func (auth *AuthService) ResourceAllowed(c *gin.Context, context config.UserContext, labels config.Labels) bool { if context.OAuth { log.Debug().Msg("Checking OAuth whitelist") return utils.CheckFilter(labels.OAuth.Whitelist, context.Email) @@ -334,12 +325,11 @@ func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext, lab return utils.CheckFilter(labels.Users, context.Username) } -func (auth *Auth) OAuthGroup(c *gin.Context, context types.UserContext, labels types.Labels) bool { +func (auth *AuthService) OAuthGroup(c *gin.Context, context config.UserContext, labels config.Labels) bool { if labels.OAuth.Groups == "" { return true } - // Check if we are using the generic oauth provider if context.Provider != "generic" { log.Debug().Msg("Not using generic provider, skipping group check") return true @@ -351,7 +341,6 @@ func (auth *Auth) OAuthGroup(c *gin.Context, context types.UserContext, labels t // For every group check if it is in the required groups for _, group := range oauthGroups { if utils.CheckFilter(labels.OAuth.Groups, group) { - log.Debug().Str("group", group).Msg("Group is in required groups") return true } } @@ -361,18 +350,15 @@ func (auth *Auth) OAuthGroup(c *gin.Context, context types.UserContext, labels t return false } -func (auth *Auth) AuthEnabled(uri string, labels types.Labels) (bool, error) { +func (auth *AuthService) AuthEnabled(uri string, labels config.Labels) (bool, error) { // If the label is empty, auth is enabled if labels.Allowed == "" { return true, nil } - // Compile regex regex, err := regexp.Compile(labels.Allowed) - // If there is an error, invalid regex, auth enabled if err != nil { - log.Error().Err(err).Msg("Invalid regex") return true, err } @@ -385,27 +371,28 @@ func (auth *Auth) AuthEnabled(uri string, labels types.Labels) (bool, error) { return true, nil } -func (auth *Auth) GetBasicAuth(c *gin.Context) *types.User { +func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User { username, password, ok := c.Request.BasicAuth() if !ok { + log.Debug().Msg("No basic auth provided") return nil } - return &types.User{ + return &config.User{ Username: username, Password: password, } } -func (auth *Auth) CheckIP(labels types.Labels, ip string) bool { +func (auth *AuthService) CheckIP(labels config.Labels, ip string) bool { // Check if the IP is in block list for _, blocked := range labels.IP.Block { res, err := utils.FilterIP(blocked, ip) if err != nil { - log.Error().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list") + log.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list") continue } if res { - log.Warn().Str("ip", ip).Str("item", blocked).Msg("IP is in blocked list, denying access") + log.Debug().Str("ip", ip).Str("item", blocked).Msg("IP is in blocked list, denying access") return false } } @@ -414,7 +401,7 @@ func (auth *Auth) CheckIP(labels types.Labels, ip string) bool { for _, allowed := range labels.IP.Allow { res, err := utils.FilterIP(allowed, ip) if err != nil { - log.Error().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list") + log.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list") continue } if res { @@ -425,7 +412,7 @@ func (auth *Auth) CheckIP(labels types.Labels, ip string) bool { // If not in allowed range and allowed range is not empty, deny access if len(labels.IP.Allow) > 0 { - log.Warn().Str("ip", ip).Msg("IP not in allow list, denying access") + log.Debug().Str("ip", ip).Msg("IP not in allow list, denying access") return false } @@ -433,12 +420,12 @@ func (auth *Auth) CheckIP(labels types.Labels, ip string) bool { return true } -func (auth *Auth) BypassedIP(labels types.Labels, ip string) bool { +func (auth *AuthService) BypassedIP(labels config.Labels, ip string) bool { // For every IP in the bypass list, check if the IP matches for _, bypassed := range labels.IP.Bypass { res, err := utils.FilterIP(bypassed, ip) if err != nil { - log.Error().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list") + log.Warn().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list") continue } if res { diff --git a/internal/docker/docker.go b/internal/service/docker_service.go similarity index 64% rename from internal/docker/docker.go rename to internal/service/docker_service.go index f5a04681..41eb07c9 100644 --- a/internal/docker/docker.go +++ b/internal/service/docker_service.go @@ -1,37 +1,42 @@ -package docker +package service import ( "context" "strings" - "tinyauth/internal/types" + "tinyauth/internal/config" "tinyauth/internal/utils" + "slices" + container "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" "github.com/rs/zerolog/log" ) -type Docker struct { +type DockerService struct { Client *client.Client Context context.Context } -func NewDocker() (*Docker, error) { +func NewDockerService() *DockerService { + return &DockerService{} +} + +func (docker *DockerService) Init() error { client, err := client.NewClientWithOpts(client.FromEnv) if err != nil { - return nil, err + return err } ctx := context.Background() client.NegotiateAPIVersion(ctx) - return &Docker{ - Client: client, - Context: ctx, - }, nil + docker.Client = client + docker.Context = ctx + return nil } -func (docker *Docker) GetContainers() ([]container.Summary, error) { +func (docker *DockerService) GetContainers() ([]container.Summary, error) { containers, err := docker.Client.ContainerList(docker.Context, container.ListOptions{}) if err != nil { return nil, err @@ -39,7 +44,7 @@ func (docker *Docker) GetContainers() ([]container.Summary, error) { return containers, nil } -func (docker *Docker) InspectContainer(containerId string) (container.InspectResponse, error) { +func (docker *DockerService) InspectContainer(containerId string) (container.InspectResponse, error) { inspect, err := docker.Client.ContainerInspect(docker.Context, containerId) if err != nil { return container.InspectResponse{}, err @@ -47,25 +52,22 @@ func (docker *Docker) InspectContainer(containerId string) (container.InspectRes return inspect, nil } -func (docker *Docker) DockerConnected() bool { +func (docker *DockerService) DockerConnected() bool { _, err := docker.Client.Ping(docker.Context) return err == nil } -func (docker *Docker) GetLabels(app string, domain string) (types.Labels, error) { +func (docker *DockerService) GetLabels(app string, domain string) (config.Labels, error) { isConnected := docker.DockerConnected() if !isConnected { log.Debug().Msg("Docker not connected, returning empty labels") - return types.Labels{}, nil + return config.Labels{}, nil } - log.Debug().Msg("Getting containers") - containers, err := docker.GetContainers() if err != nil { - log.Error().Err(err).Msg("Error getting containers") - return types.Labels{}, err + return config.Labels{}, err } for _, container := range containers { @@ -75,8 +77,6 @@ func (docker *Docker) GetLabels(app string, domain string) (types.Labels, error) continue } - log.Debug().Str("id", inspect.ID).Msg("Getting labels for container") - labels, err := utils.GetLabels(inspect.Config.Labels) if err != nil { log.Warn().Str("id", container.ID).Err(err).Msg("Error getting container labels, skipping") @@ -84,11 +84,9 @@ func (docker *Docker) GetLabels(app string, domain string) (types.Labels, error) } // Check if the container matches the ID or domain - for _, lDomain := range labels.Domain { - if lDomain == domain { - log.Debug().Str("id", inspect.ID).Msg("Found matching container by domain") - return labels, nil - } + if slices.Contains(labels.Domain, domain) { + log.Debug().Str("id", inspect.ID).Msg("Found matching container by domain") + return labels, nil } if strings.TrimPrefix(inspect.Name, "/") == app { @@ -98,5 +96,5 @@ func (docker *Docker) GetLabels(app string, domain string) (types.Labels, error) } log.Debug().Msg("No matching container found, returning empty labels") - return types.Labels{}, nil + return config.Labels{}, nil } diff --git a/internal/service/generic_oauth_service.go b/internal/service/generic_oauth_service.go new file mode 100644 index 00000000..c16384db --- /dev/null +++ b/internal/service/generic_oauth_service.go @@ -0,0 +1,117 @@ +package service + +import ( + "context" + "crypto/rand" + "crypto/tls" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + "tinyauth/internal/config" + + "golang.org/x/oauth2" +) + +type GenericOAuthService struct { + Config oauth2.Config + Context context.Context + Token *oauth2.Token + Verifier string + InsecureSkipVerify bool + UserinfoURL string +} + +func NewGenericOAuthService(config config.OAuthServiceConfig) *GenericOAuthService { + return &GenericOAuthService{ + Config: oauth2.Config{ + ClientID: config.ClientID, + ClientSecret: config.ClientSecret, + RedirectURL: config.RedirectURL, + Scopes: config.Scopes, + Endpoint: oauth2.Endpoint{ + AuthURL: config.AuthURL, + TokenURL: config.TokenURL, + }, + }, + InsecureSkipVerify: config.InsecureSkipVerify, + UserinfoURL: config.UserinfoURL, + } +} + +func (generic *GenericOAuthService) Init() error { + transport := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: generic.InsecureSkipVerify, + MinVersion: tls.VersionTLS12, + }, + } + + httpClient := &http.Client{ + Transport: transport, + } + + ctx := context.Background() + + ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) + verifier := oauth2.GenerateVerifier() + + generic.Context = ctx + generic.Verifier = verifier + return nil +} + +func (generic *GenericOAuthService) GenerateState() string { + b := make([]byte, 128) + _, err := rand.Read(b) + if err != nil { + return base64.RawURLEncoding.EncodeToString(fmt.Appendf(nil, "state-%d", time.Now().UnixNano())) + } + state := base64.RawURLEncoding.EncodeToString(b) + return state +} + +func (generic *GenericOAuthService) GetAuthURL(state string) string { + return generic.Config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(generic.Verifier)) +} + +func (generic *GenericOAuthService) VerifyCode(code string) error { + token, err := generic.Config.Exchange(generic.Context, code, oauth2.VerifierOption(generic.Verifier)) + + if err != nil { + return err + } + + generic.Token = token + return nil +} + +func (generic *GenericOAuthService) Userinfo() (config.Claims, error) { + var user config.Claims + + client := generic.Config.Client(generic.Context, generic.Token) + + res, err := client.Get(generic.UserinfoURL) + if err != nil { + return user, err + } + defer res.Body.Close() + + if res.StatusCode < 200 || res.StatusCode >= 300 { + return user, fmt.Errorf("request failed with status: %s", res.Status) + } + + body, err := io.ReadAll(res.Body) + if err != nil { + return user, err + } + + err = json.Unmarshal(body, &user) + if err != nil { + return user, err + } + + return user, nil +} diff --git a/internal/service/github_oauth_service.go b/internal/service/github_oauth_service.go new file mode 100644 index 00000000..7f8466b9 --- /dev/null +++ b/internal/service/github_oauth_service.go @@ -0,0 +1,169 @@ +package service + +import ( + "context" + "crypto/rand" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "time" + "tinyauth/internal/config" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/endpoints" +) + +var GithubOAuthScopes = []string{"user:email", "read:user"} + +type GithubEmailResponse []struct { + Email string `json:"email"` + Primary bool `json:"primary"` +} + +type GithubUserInfoResponse struct { + Login string `json:"login"` + Name string `json:"name"` +} + +type GithubOAuthService struct { + Config oauth2.Config + Context context.Context + Token *oauth2.Token + Verifier string +} + +func NewGithubOAuthService(config config.OAuthServiceConfig) *GithubOAuthService { + return &GithubOAuthService{ + Config: oauth2.Config{ + ClientID: config.ClientID, + ClientSecret: config.ClientSecret, + RedirectURL: config.RedirectURL, + Scopes: GithubOAuthScopes, + Endpoint: endpoints.GitHub, + }, + } +} + +func (github *GithubOAuthService) Init() error { + httpClient := &http.Client{} + ctx := context.Background() + ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) + verifier := oauth2.GenerateVerifier() + + github.Context = ctx + github.Verifier = verifier + return nil +} + +func (github *GithubOAuthService) GenerateState() string { + b := make([]byte, 128) + _, err := rand.Read(b) + if err != nil { + return base64.RawURLEncoding.EncodeToString(fmt.Appendf(nil, "state-%d", time.Now().UnixNano())) + } + state := base64.RawURLEncoding.EncodeToString(b) + return state +} + +func (github *GithubOAuthService) GetAuthURL(state string) string { + return github.Config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(github.Verifier)) +} + +func (github *GithubOAuthService) VerifyCode(code string) error { + token, err := github.Config.Exchange(github.Context, code, oauth2.VerifierOption(github.Verifier)) + + if err != nil { + return err + } + + github.Token = token + return nil +} + +func (github *GithubOAuthService) Userinfo() (config.Claims, error) { + var user config.Claims + + client := github.Config.Client(github.Context, github.Token) + + req, err := http.NewRequest("GET", "https://api.github.com/user", nil) + if err != nil { + return user, err + } + + req.Header.Set("Accept", "application/vnd.github+json") + + res, err := client.Do(req) + if err != nil { + return user, err + } + defer res.Body.Close() + + if res.StatusCode < 200 || res.StatusCode >= 300 { + return user, fmt.Errorf("request failed with status: %s", res.Status) + } + + body, err := io.ReadAll(res.Body) + if err != nil { + return user, err + } + + var userInfo GithubUserInfoResponse + + err = json.Unmarshal(body, &userInfo) + if err != nil { + return user, err + } + + req, err = http.NewRequest("GET", "https://api.github.com/user/emails", nil) + if err != nil { + return user, err + } + + req.Header.Set("Accept", "application/vnd.github+json") + + res, err = client.Do(req) + if err != nil { + return user, err + } + defer res.Body.Close() + + if res.StatusCode < 200 || res.StatusCode >= 300 { + return user, fmt.Errorf("request failed with status: %s", res.Status) + } + + body, err = io.ReadAll(res.Body) + if err != nil { + return user, err + } + + var emails GithubEmailResponse + + err = json.Unmarshal(body, &emails) + if err != nil { + return user, err + } + + for _, email := range emails { + if email.Primary { + user.Email = email.Email + break + } + } + + if len(emails) == 0 { + return user, errors.New("no emails found") + } + + // Use first available email if no primary email was found + if user.Email == "" { + user.Email = emails[0].Email + } + + user.PreferredUsername = userInfo.Login + user.Name = userInfo.Name + + return user, nil +} diff --git a/internal/service/google_oauth_service.go b/internal/service/google_oauth_service.go new file mode 100644 index 00000000..1605a855 --- /dev/null +++ b/internal/service/google_oauth_service.go @@ -0,0 +1,113 @@ +package service + +import ( + "context" + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + "tinyauth/internal/config" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/endpoints" +) + +var GoogleOAuthScopes = []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"} + +type GoogleUserInfoResponse struct { + Email string `json:"email"` + Name string `json:"name"` +} + +type GoogleOAuthService struct { + Config oauth2.Config + Context context.Context + Token *oauth2.Token + Verifier string +} + +func NewGoogleOAuthService(config config.OAuthServiceConfig) *GoogleOAuthService { + return &GoogleOAuthService{ + Config: oauth2.Config{ + ClientID: config.ClientID, + ClientSecret: config.ClientSecret, + RedirectURL: config.RedirectURL, + Scopes: GoogleOAuthScopes, + Endpoint: endpoints.Google, + }, + } +} + +func (google *GoogleOAuthService) Init() error { + httpClient := &http.Client{} + ctx := context.Background() + ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) + verifier := oauth2.GenerateVerifier() + + google.Context = ctx + google.Verifier = verifier + return nil +} + +func (oauth *GoogleOAuthService) GenerateState() string { + b := make([]byte, 128) + _, err := rand.Read(b) + if err != nil { + return base64.RawURLEncoding.EncodeToString(fmt.Appendf(nil, "state-%d", time.Now().UnixNano())) + } + state := base64.RawURLEncoding.EncodeToString(b) + return state +} + +func (google *GoogleOAuthService) GetAuthURL(state string) string { + return google.Config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(google.Verifier)) +} + +func (google *GoogleOAuthService) VerifyCode(code string) error { + token, err := google.Config.Exchange(google.Context, code, oauth2.VerifierOption(google.Verifier)) + + if err != nil { + return err + } + + google.Token = token + return nil +} + +func (google *GoogleOAuthService) Userinfo() (config.Claims, error) { + var user config.Claims + + client := google.Config.Client(google.Context, google.Token) + + res, err := client.Get("https://www.googleapis.com/userinfo/v2/me") + if err != nil { + return config.Claims{}, err + } + defer res.Body.Close() + + if res.StatusCode < 200 || res.StatusCode >= 300 { + return user, fmt.Errorf("request failed with status: %s", res.Status) + } + + body, err := io.ReadAll(res.Body) + if err != nil { + return config.Claims{}, err + } + + var userInfo GoogleUserInfoResponse + + err = json.Unmarshal(body, &userInfo) + if err != nil { + return config.Claims{}, err + } + + user.PreferredUsername = strings.Split(userInfo.Email, "@")[0] + user.Name = userInfo.Name + user.Email = userInfo.Email + + return user, nil +} diff --git a/internal/ldap/ldap.go b/internal/service/ldap_service.go similarity index 61% rename from internal/ldap/ldap.go rename to internal/service/ldap_service.go index 61578d76..8576c4d7 100644 --- a/internal/ldap/ldap.go +++ b/internal/service/ldap_service.go @@ -1,30 +1,40 @@ -package ldap +package service import ( "context" "crypto/tls" "fmt" "time" - "tinyauth/internal/types" "github.com/cenkalti/backoff/v5" ldapgo "github.com/go-ldap/ldap/v3" "github.com/rs/zerolog/log" ) -type LDAP struct { - Config types.LdapConfig +type LdapServiceConfig struct { + Address string + BindDN string + BindPassword string + BaseDN string + Insecure bool + SearchFilter string +} + +type LdapService struct { + Config LdapServiceConfig Conn *ldapgo.Conn } -func NewLDAP(config types.LdapConfig) (*LDAP, error) { - ldap := &LDAP{ +func NewLdapService(config LdapServiceConfig) *LdapService { + return &LdapService{ Config: config, } +} +func (ldap *LdapService) Init() error { _, err := ldap.connect() if err != nil { - return nil, fmt.Errorf("failed to connect to LDAP server: %w", err) + return fmt.Errorf("failed to connect to LDAP server: %w", err) } go func() { @@ -41,65 +51,63 @@ func NewLDAP(config types.LdapConfig) (*LDAP, error) { } }() - return ldap, nil + return nil } -func (l *LDAP) connect() (*ldapgo.Conn, error) { - log.Debug().Msg("Connecting to LDAP server") - conn, err := ldapgo.DialURL(l.Config.Address, ldapgo.DialWithTLSConfig(&tls.Config{ - InsecureSkipVerify: l.Config.Insecure, +func (ldap *LdapService) connect() (*ldapgo.Conn, error) { + conn, err := ldapgo.DialURL(ldap.Config.Address, ldapgo.DialWithTLSConfig(&tls.Config{ + InsecureSkipVerify: ldap.Config.Insecure, MinVersion: tls.VersionTLS12, })) if err != nil { return nil, err } - log.Debug().Msg("Binding to LDAP server") - err = conn.Bind(l.Config.BindDN, l.Config.BindPassword) + err = conn.Bind(ldap.Config.BindDN, ldap.Config.BindPassword) if err != nil { return nil, err } // Set and return the connection - l.Conn = conn + ldap.Conn = conn return conn, nil } -func (l *LDAP) Search(username string) (string, error) { +func (ldap *LdapService) Search(username string) (string, error) { // Escape the username to prevent LDAP injection escapedUsername := ldapgo.EscapeFilter(username) - filter := fmt.Sprintf(l.Config.SearchFilter, escapedUsername) + filter := fmt.Sprintf(ldap.Config.SearchFilter, escapedUsername) searchRequest := ldapgo.NewSearchRequest( - l.Config.BaseDN, + ldap.Config.BaseDN, ldapgo.ScopeWholeSubtree, ldapgo.NeverDerefAliases, 0, 0, false, filter, []string{"dn"}, nil, ) - searchResult, err := l.Conn.Search(searchRequest) + searchResult, err := ldap.Conn.Search(searchRequest) if err != nil { return "", err } if len(searchResult.Entries) != 1 { - return "", fmt.Errorf("err multiple or no entries found for user %s", username) + return "", fmt.Errorf("multiple or no entries found for user %s", username) } userDN := searchResult.Entries[0].DN return userDN, nil } -func (l *LDAP) Bind(userDN string, password string) error { - err := l.Conn.Bind(userDN, password) +func (ldap *LdapService) Bind(userDN string, password string) error { + err := ldap.Conn.Bind(userDN, password) if err != nil { return err } return nil } -func (l *LDAP) heartbeat() error { +func (ldap *LdapService) heartbeat() error { log.Debug().Msg("Performing LDAP connection heartbeat") searchRequest := ldapgo.NewSearchRequest( @@ -110,7 +118,7 @@ func (l *LDAP) heartbeat() error { nil, ) - _, err := l.Conn.Search(searchRequest) + _, err := ldap.Conn.Search(searchRequest) if err != nil { return err } @@ -119,7 +127,7 @@ func (l *LDAP) heartbeat() error { return nil } -func (l *LDAP) reconnect() error { +func (ldap *LdapService) reconnect() error { log.Info().Msg("Reconnecting to LDAP server") exp := backoff.NewExponentialBackOff() @@ -129,10 +137,10 @@ func (l *LDAP) reconnect() error { exp.Reset() operation := func() (*ldapgo.Conn, error) { - l.Conn.Close() - conn, err := l.connect() + ldap.Conn.Close() + conn, err := ldap.connect() if err != nil { - return nil, nil + return nil, err } return conn, nil } diff --git a/internal/service/oauth_broker_service.go b/internal/service/oauth_broker_service.go new file mode 100644 index 00000000..6b5b1e6f --- /dev/null +++ b/internal/service/oauth_broker_service.go @@ -0,0 +1,76 @@ +package service + +import ( + "errors" + "tinyauth/internal/config" + + "github.com/rs/zerolog/log" +) + +type OAuthService interface { + Init() error + GenerateState() string + GetAuthURL(state string) string + VerifyCode(code string) error + Userinfo() (config.Claims, error) +} + +type OAuthBrokerService struct { + Services map[string]OAuthService + Configs map[string]config.OAuthServiceConfig +} + +func NewOAuthBrokerService(configs map[string]config.OAuthServiceConfig) *OAuthBrokerService { + return &OAuthBrokerService{ + Services: make(map[string]OAuthService), + Configs: configs, + } +} + +func (broker *OAuthBrokerService) Init() error { + for name, cfg := range broker.Configs { + switch name { + case "github": + service := NewGithubOAuthService(cfg) + broker.Services[name] = service + case "google": + service := NewGoogleOAuthService(cfg) + broker.Services[name] = service + default: + service := NewGenericOAuthService(cfg) + broker.Services[name] = service + } + } + + for name, service := range broker.Services { + err := service.Init() + if err != nil { + log.Error().Err(err).Msgf("Failed to initialize OAuth service: %s", name) + return err + } + log.Info().Msgf("Initialized OAuth service: %s", name) + } + + return nil +} + +func (broker *OAuthBrokerService) GetConfiguredServices() []string { + services := make([]string, 0, len(broker.Services)) + for name := range broker.Services { + services = append(services, name) + } + return services +} + +func (broker *OAuthBrokerService) GetService(name string) (OAuthService, bool) { + service, exists := broker.Services[name] + return service, exists +} + +func (broker *OAuthBrokerService) GetUser(service string) (config.Claims, error) { + oauthService, exists := broker.Services[service] + if !exists { + return config.Claims{}, errors.New("oauth service not found") + } + return oauthService.Userinfo() +} diff --git a/internal/types/api.go b/internal/types/api.go deleted file mode 100644 index fbf8bf77..00000000 --- a/internal/types/api.go +++ /dev/null @@ -1,62 +0,0 @@ -package types - -// LoginQuery is the query parameters for the login endpoint -type LoginQuery struct { - RedirectURI string `url:"redirect_uri"` -} - -// LoginRequest is the request body for the login endpoint -type LoginRequest struct { - Username string `json:"username"` - Password string `json:"password"` -} - -// OAuthRequest is the request for the OAuth endpoint -type OAuthRequest struct { - Provider string `uri:"provider" binding:"required"` -} - -// UnauthorizedQuery is the query parameters for the unauthorized endpoint -type UnauthorizedQuery struct { - Username string `url:"username"` - Resource string `url:"resource"` - GroupErr bool `url:"groupErr"` - IP string `url:"ip"` -} - -// Proxy is the uri parameters for the proxy endpoint -type Proxy struct { - Proxy string `uri:"proxy" binding:"required"` -} - -// User Context response is the response for the user context endpoint -type UserContextResponse struct { - Status int `json:"status"` - Message string `json:"message"` - IsLoggedIn bool `json:"isLoggedIn"` - Username string `json:"username"` - Name string `json:"name"` - Email string `json:"email"` - Provider string `json:"provider"` - Oauth bool `json:"oauth"` - TotpPending bool `json:"totpPending"` -} - -// App Context is the response for the app context endpoint -type AppContext struct { - Status int `json:"status"` - Message string `json:"message"` - ConfiguredProviders []string `json:"configuredProviders"` - DisableContinue bool `json:"disableContinue"` - Title string `json:"title"` - GenericName string `json:"genericName"` - Domain string `json:"domain"` - ForgotPasswordMessage string `json:"forgotPasswordMessage"` - BackgroundImage string `json:"backgroundImage"` - OAuthAutoRedirect string `json:"oauthAutoRedirect"` -} - -// Totp request is the request for the totp endpoint -type TotpRequest struct { - Code string `json:"code"` -} diff --git a/internal/types/types.go b/internal/types/types.go deleted file mode 100644 index 2c40ae55..00000000 --- a/internal/types/types.go +++ /dev/null @@ -1,59 +0,0 @@ -package types - -import ( - "time" - "tinyauth/internal/oauth" -) - -// User is the struct for a user -type User struct { - Username string - Password string - TotpSecret string -} - -// UserSearch is the response of the get user -type UserSearch struct { - Username string - Type string // "local", "ldap" or empty -} - -// Users is a list of users -type Users []User - -// OAuthProviders is the struct for the OAuth providers -type OAuthProviders struct { - Github *oauth.OAuth - Google *oauth.OAuth - Microsoft *oauth.OAuth -} - -// SessionCookie is the cookie for the session (exculding the expiry) -type SessionCookie struct { - Username string - Name string - Email string - Provider string - TotpPending bool - OAuthGroups string -} - -// UserContext is the context for the user -type UserContext struct { - Username string - Name string - Email string - IsLoggedIn bool - OAuth bool - Provider string - TotpPending bool - OAuthGroups string - TotpEnabled bool -} - -// LoginAttempt tracks information about login attempts for rate limiting -type LoginAttempt struct { - FailedAttempts int - LastAttempt time.Time - LockedUntil time.Time -} diff --git a/internal/utils/app_utils.go b/internal/utils/app_utils.go new file mode 100644 index 00000000..85a87542 --- /dev/null +++ b/internal/utils/app_utils.go @@ -0,0 +1,123 @@ +package utils + +import ( + "errors" + "net" + "net/url" + "strings" + "tinyauth/internal/config" + + "github.com/gin-gonic/gin" + + "github.com/rs/zerolog" +) + +// Get upper domain parses a hostname and returns the upper domain (e.g. sub1.sub2.domain.com -> sub2.domain.com) +func GetUpperDomain(appUrl string) (string, error) { + appUrlParsed, err := url.Parse(appUrl) + if err != nil { + return "", err + } + + host := appUrlParsed.Hostname() + + if netIP := net.ParseIP(host); netIP != nil { + return "", errors.New("IP addresses are not allowed") + } + + urlParts := strings.Split(host, ".") + + if len(urlParts) < 2 { + return "", errors.New("invalid domain, must be at least second level domain") + } + + return strings.Join(urlParts[1:], "."), nil +} + +func ParseFileToLine(content string) string { + lines := strings.Split(content, "\n") + users := make([]string, 0) + + for _, line := range lines { + if strings.TrimSpace(line) == "" { + continue + } + users = append(users, strings.TrimSpace(line)) + } + + return strings.Join(users, ",") +} + +func Filter[T any](slice []T, test func(T) bool) (res []T) { + for _, value := range slice { + if test(value) { + res = append(res, value) + } + } + return res +} + +func GetContext(c *gin.Context) (config.UserContext, error) { + userContextValue, exists := c.Get("context") + + if !exists { + return config.UserContext{}, errors.New("no user context in request") + } + + userContext, ok := userContextValue.(*config.UserContext) + + if !ok { + return config.UserContext{}, errors.New("invalid user context in request") + } + + return *userContext, nil +} + +func IsRedirectSafe(redirectURL string, domain string) bool { + if redirectURL == "" { + return false + } + + parsedURL, err := url.Parse(redirectURL) + + if err != nil { + return false + } + + if !parsedURL.IsAbs() { + return false + } + + upper, err := GetUpperDomain(redirectURL) + + if err != nil { + return false + } + + if upper != domain { + return false + } + + return true +} + +func GetLogLevel(level string) zerolog.Level { + switch strings.ToLower(level) { + case "trace": + return zerolog.TraceLevel + case "debug": + return zerolog.DebugLevel + case "info": + return zerolog.InfoLevel + case "warn": + return zerolog.WarnLevel + case "error": + return zerolog.ErrorLevel + case "fatal": + return zerolog.FatalLevel + case "panic": + return zerolog.PanicLevel + default: + return zerolog.InfoLevel + } +} diff --git a/internal/utils/fs_utils.go b/internal/utils/fs_utils.go new file mode 100644 index 00000000..8b9f28bf --- /dev/null +++ b/internal/utils/fs_utils.go @@ -0,0 +1,17 @@ +package utils + +import "os" + +func ReadFile(file string) (string, error) { + _, err := os.Stat(file) + if err != nil { + return "", err + } + + data, err := os.ReadFile(file) + if err != nil { + return "", err + } + + return string(data), nil +} diff --git a/internal/utils/label_utils.go b/internal/utils/label_utils.go new file mode 100644 index 00000000..f10092df --- /dev/null +++ b/internal/utils/label_utils.go @@ -0,0 +1,48 @@ +package utils + +import ( + "net/http" + "strings" + "tinyauth/internal/config" + + "github.com/traefik/paerser/parser" +) + +func GetLabels(labels map[string]string) (config.Labels, error) { + var labelsParsed config.Labels + + err := parser.Decode(labels, &labelsParsed, "tinyauth", "tinyauth.users", "tinyauth.allowed", "tinyauth.headers", "tinyauth.domain", "tinyauth.basic", "tinyauth.oauth", "tinyauth.ip") + if err != nil { + return config.Labels{}, err + } + + return labelsParsed, nil +} + +func ParseHeaders(headers []string) map[string]string { + headerMap := make(map[string]string) + for _, header := range headers { + split := strings.SplitN(header, "=", 2) + if len(split) != 2 || strings.TrimSpace(split[0]) == "" || strings.TrimSpace(split[1]) == "" { + continue + } + key := SanitizeHeader(strings.TrimSpace(split[0])) + if strings.ContainsAny(key, " \t") { + continue + } + key = http.CanonicalHeaderKey(key) + value := SanitizeHeader(strings.TrimSpace(split[1])) + headerMap[key] = value + } + return headerMap +} + +func SanitizeHeader(header string) string { + return strings.Map(func(r rune) rune { + // Allow only printable ASCII characters (32-126) and safe whitespace (space, tab) + if r == ' ' || r == '\t' || (r >= 32 && r <= 126) { + return r + } + return -1 + }, header) +} diff --git a/internal/utils/security_utils.go b/internal/utils/security_utils.go new file mode 100644 index 00000000..a0319008 --- /dev/null +++ b/internal/utils/security_utils.go @@ -0,0 +1,124 @@ +package utils + +import ( + "bytes" + "crypto/sha256" + "encoding/base64" + "errors" + "io" + "net" + "regexp" + "strings" + + "github.com/google/uuid" + "golang.org/x/crypto/hkdf" +) + +func GetSecret(conf string, file string) string { + if conf == "" && file == "" { + return "" + } + + if conf != "" { + return conf + } + + contents, err := ReadFile(file) + if err != nil { + return "" + } + + return ParseSecretFile(contents) +} + +func ParseSecretFile(contents string) string { + lines := strings.Split(contents, "\n") + + for _, line := range lines { + if strings.TrimSpace(line) == "" { + continue + } + return strings.TrimSpace(line) + } + + return "" +} + +func GetBasicAuth(username string, password string) string { + auth := username + ":" + password + return base64.StdEncoding.EncodeToString([]byte(auth)) +} + +func DeriveKey(secret string, info string) (string, error) { + hash := sha256.New + hkdf := hkdf.New(hash, []byte(secret), nil, []byte(info)) // I am not using a salt because I just want two different keys from one secret, maybe bad practice + key := make([]byte, 24) + + _, err := io.ReadFull(hkdf, key) + if err != nil { + return "", err + } + + if bytes.Equal(key, make([]byte, 24)) { + return "", errors.New("derived key is empty") + } + + encodedKey := base64.StdEncoding.EncodeToString(key) + return encodedKey, nil +} + +func FilterIP(filter string, ip string) (bool, error) { + ipAddr := net.ParseIP(ip) + + if strings.Contains(filter, "/") { + _, cidr, err := net.ParseCIDR(filter) + if err != nil { + return false, err + } + return cidr.Contains(ipAddr), nil + } + + ipFilter := net.ParseIP(filter) + if ipFilter == nil { + return false, errors.New("invalid IP address in filter") + } + + if ipFilter.Equal(ipAddr) { + return true, nil + } + + return false, nil +} + +func CheckFilter(filter string, str string) bool { + if len(strings.TrimSpace(filter)) == 0 { + return true + } + + if strings.HasPrefix(filter, "/") && strings.HasSuffix(filter, "/") { + re, err := regexp.Compile(filter[1 : len(filter)-1]) + if err != nil { + return false + } + + if re.MatchString(strings.TrimSpace(str)) { + return true + } + } + + filterSplit := strings.Split(filter, ",") + + for _, item := range filterSplit { + if strings.TrimSpace(item) == strings.TrimSpace(str) { + return true + } + } + + return false +} + +func GenerateIdentifier(str string) string { + uuid := uuid.NewSHA1(uuid.NameSpaceURL, []byte(str)) + uuidString := uuid.String() + return strings.Split(uuidString, "-")[0] +} diff --git a/internal/utils/string_utils.go b/internal/utils/string_utils.go new file mode 100644 index 00000000..8a629adc --- /dev/null +++ b/internal/utils/string_utils.go @@ -0,0 +1,30 @@ +package utils + +import ( + "strings" +) + +func Capitalize(str string) string { + if len(str) == 0 { + return "" + } + return strings.ToUpper(string([]rune(str)[0])) + string([]rune(str)[1:]) +} + +func CoalesceToString(value any) string { + switch v := value.(type) { + case []any: + strs := make([]string, 0, len(v)) + for _, item := range v { + if str, ok := item.(string); ok { + strs = append(strs, str) + continue + } + } + return strings.Join(strs, ",") + case string: + return v + default: + return "" + } +} diff --git a/internal/utils/user_utils.go b/internal/utils/user_utils.go new file mode 100644 index 00000000..0044db4a --- /dev/null +++ b/internal/utils/user_utils.go @@ -0,0 +1,92 @@ +package utils + +import ( + "errors" + "strings" + "tinyauth/internal/config" +) + +func ParseUsers(users string) ([]config.User, error) { + var usersParsed []config.User + + users = strings.TrimSpace(users) + + if users == "" { + return []config.User{}, nil + } + + userList := strings.Split(users, ",") + + if len(userList) == 0 { + return []config.User{}, errors.New("invalid user format") + } + + for _, user := range userList { + if strings.TrimSpace(user) == "" { + continue + } + parsed, err := ParseUser(strings.TrimSpace(user)) + if err != nil { + return []config.User{}, err + } + usersParsed = append(usersParsed, parsed) + } + + return usersParsed, nil +} + +func GetUsers(conf string, file string) ([]config.User, error) { + var users string + + if conf == "" && file == "" { + return []config.User{}, nil + } + + if conf != "" { + users += conf + } + + if file != "" { + contents, err := ReadFile(file) + if err != nil { + return []config.User{}, err + } + if users != "" { + users += "," + } + users += ParseFileToLine(contents) + } + + return ParseUsers(users) +} + +func ParseUser(user string) (config.User, error) { + if strings.Contains(user, "$$") { + user = strings.ReplaceAll(user, "$$", "$") + } + + userSplit := strings.Split(user, ":") + + if len(userSplit) < 2 || len(userSplit) > 3 { + return config.User{}, errors.New("invalid user format") + } + + for _, userPart := range userSplit { + if strings.TrimSpace(userPart) == "" { + return config.User{}, errors.New("invalid user format") + } + } + + if len(userSplit) == 2 { + return config.User{ + Username: strings.TrimSpace(userSplit[0]), + Password: strings.TrimSpace(userSplit[1]), + }, nil + } + + return config.User{ + Username: strings.TrimSpace(userSplit[0]), + Password: strings.TrimSpace(userSplit[1]), + TotpSecret: strings.TrimSpace(userSplit[2]), + }, nil +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go deleted file mode 100644 index 39b1518f..00000000 --- a/internal/utils/utils.go +++ /dev/null @@ -1,350 +0,0 @@ -package utils - -import ( - "bytes" - "crypto/sha256" - "encoding/base64" - "errors" - "io" - "net" - "net/url" - "os" - "regexp" - "strings" - "tinyauth/internal/types" - - "github.com/traefik/paerser/parser" - "golang.org/x/crypto/hkdf" - - "github.com/google/uuid" - "github.com/rs/zerolog/log" -) - -// Parses a list of comma separated users in a struct -func ParseUsers(users string) (types.Users, error) { - log.Debug().Msg("Parsing users") - - var usersParsed types.Users - - userList := strings.Split(users, ",") - - if len(userList) == 0 { - return types.Users{}, errors.New("invalid user format") - } - - for _, user := range userList { - parsed, err := ParseUser(user) - if err != nil { - return types.Users{}, err - } - usersParsed = append(usersParsed, parsed) - } - - log.Debug().Msg("Parsed users") - return usersParsed, nil -} - -// Get upper domain parses a hostname and returns the upper domain (e.g. sub1.sub2.domain.com -> sub2.domain.com) -func GetUpperDomain(urlSrc string) (string, error) { - urlParsed, err := url.Parse(urlSrc) - if err != nil { - return "", err - } - - urlSplitted := strings.Split(urlParsed.Hostname(), ".") - urlFinal := strings.Join(urlSplitted[1:], ".") - - return urlFinal, nil -} - -// Reads a file and returns the contents -func ReadFile(file string) (string, error) { - _, err := os.Stat(file) - if err != nil { - return "", err - } - - data, err := os.ReadFile(file) - if err != nil { - return "", err - } - - return string(data), nil -} - -// Parses a file into a comma separated list of users -func ParseFileToLine(content string) string { - lines := strings.Split(content, "\n") - users := make([]string, 0) - - for _, line := range lines { - if strings.TrimSpace(line) == "" { - continue - } - users = append(users, strings.TrimSpace(line)) - } - - return strings.Join(users, ",") -} - -// Get the secret from the config or file -func GetSecret(conf string, file string) string { - if conf == "" && file == "" { - return "" - } - - if conf != "" { - return conf - } - - contents, err := ReadFile(file) - if err != nil { - return "" - } - - return ParseSecretFile(contents) -} - -// Get the users from the config or file -func GetUsers(conf string, file string) (types.Users, error) { - var users string - - if conf == "" && file == "" { - return types.Users{}, nil - } - - if conf != "" { - log.Debug().Msg("Using users from config") - users += conf - } - - if file != "" { - contents, err := ReadFile(file) - if err == nil { - log.Debug().Msg("Using users from file") - if users != "" { - users += "," - } - users += ParseFileToLine(contents) - } - } - - return ParseUsers(users) -} - -// Parse the headers in a map[string]string format -func ParseHeaders(headers []string) map[string]string { - headerMap := make(map[string]string) - - for _, header := range headers { - split := strings.SplitN(header, "=", 2) - if len(split) != 2 || strings.TrimSpace(split[0]) == "" || strings.TrimSpace(split[1]) == "" { - log.Warn().Str("header", header).Msg("Invalid header format, skipping") - continue - } - key := SanitizeHeader(strings.TrimSpace(split[0])) - value := SanitizeHeader(strings.TrimSpace(split[1])) - headerMap[key] = value - } - - return headerMap -} - -// Get labels parses a map of labels into a struct with only the needed labels -func GetLabels(labels map[string]string) (types.Labels, error) { - var labelsParsed types.Labels - - err := parser.Decode(labels, &labelsParsed, "tinyauth", "tinyauth.users", "tinyauth.allowed", "tinyauth.headers", "tinyauth.domain", "tinyauth.basic", "tinyauth.oauth", "tinyauth.ip") - if err != nil { - log.Error().Err(err).Msg("Error parsing labels") - return types.Labels{}, err - } - - return labelsParsed, nil -} - -// Check if any of the OAuth providers are configured based on the client id and secret -func OAuthConfigured(config types.Config) bool { - return (config.GithubClientId != "" && config.GithubClientSecret != "") || (config.GoogleClientId != "" && config.GoogleClientSecret != "") || (config.GenericClientId != "" && config.GenericClientSecret != "") -} - -// Filter helper function -func Filter[T any](slice []T, test func(T) bool) (res []T) { - for _, value := range slice { - if test(value) { - res = append(res, value) - } - } - return res -} - -// Parse user -func ParseUser(user string) (types.User, error) { - if strings.Contains(user, "$$") { - user = strings.ReplaceAll(user, "$$", "$") - } - - userSplit := strings.Split(user, ":") - - if len(userSplit) < 2 || len(userSplit) > 3 { - return types.User{}, errors.New("invalid user format") - } - - for _, userPart := range userSplit { - if strings.TrimSpace(userPart) == "" { - return types.User{}, errors.New("invalid user format") - } - } - - if len(userSplit) == 2 { - return types.User{ - Username: strings.TrimSpace(userSplit[0]), - Password: strings.TrimSpace(userSplit[1]), - }, nil - } - - return types.User{ - Username: strings.TrimSpace(userSplit[0]), - Password: strings.TrimSpace(userSplit[1]), - TotpSecret: strings.TrimSpace(userSplit[2]), - }, nil -} - -// Parse secret file -func ParseSecretFile(contents string) string { - lines := strings.Split(contents, "\n") - - for _, line := range lines { - if strings.TrimSpace(line) == "" { - continue - } - return strings.TrimSpace(line) - } - - return "" -} - -// Check if a string matches a regex or if it is included in a comma separated list -func CheckFilter(filter string, str string) bool { - if len(strings.TrimSpace(filter)) == 0 { - return true - } - - if strings.HasPrefix(filter, "/") && strings.HasSuffix(filter, "/") { - re, err := regexp.Compile(filter[1 : len(filter)-1]) - if err != nil { - log.Error().Err(err).Msg("Error compiling regex") - return false - } - - if re.MatchString(str) { - return true - } - } - - filterSplit := strings.Split(filter, ",") - - for _, item := range filterSplit { - if strings.TrimSpace(item) == str { - return true - } - } - - return false -} - -// Capitalize just the first letter of a string -func Capitalize(str string) string { - if len(str) == 0 { - return "" - } - return strings.ToUpper(string([]rune(str)[0])) + string([]rune(str)[1:]) -} - -// Sanitize header removes all control characters from a string -func SanitizeHeader(header string) string { - return strings.Map(func(r rune) rune { - // Allow only printable ASCII characters (32-126) and safe whitespace (space, tab) - if r == ' ' || r == '\t' || (r >= 32 && r <= 126) { - return r - } - return -1 - }, header) -} - -// Generate a static identifier from a string -func GenerateIdentifier(str string) string { - uuid := uuid.NewSHA1(uuid.NameSpaceURL, []byte(str)) - uuidString := uuid.String() - log.Debug().Str("uuid", uuidString).Msg("Generated UUID") - return strings.Split(uuidString, "-")[0] -} - -// Get a basic auth header from a username and password -func GetBasicAuth(username string, password string) string { - auth := username + ":" + password - return base64.StdEncoding.EncodeToString([]byte(auth)) -} - -// Check if an IP is contained in a CIDR range/matches a single IP -func FilterIP(filter string, ip string) (bool, error) { - ipAddr := net.ParseIP(ip) - - if strings.Contains(filter, "/") { - _, cidr, err := net.ParseCIDR(filter) - if err != nil { - return false, err - } - return cidr.Contains(ipAddr), nil - } - - ipFilter := net.ParseIP(filter) - if ipFilter == nil { - return false, errors.New("invalid IP address in filter") - } - - if ipFilter.Equal(ipAddr) { - return true, nil - } - - return false, nil -} - -func DeriveKey(secret string, info string) (string, error) { - hash := sha256.New - hkdf := hkdf.New(hash, []byte(secret), nil, []byte(info)) // I am not using a salt because I just want two different keys from one secret, maybe bad practice - key := make([]byte, 24) - - _, err := io.ReadFull(hkdf, key) - if err != nil { - return "", err - } - - if bytes.Equal(key, make([]byte, 24)) { - return "", errors.New("derived key is empty") - } - - encodedKey := base64.StdEncoding.EncodeToString(key) - return encodedKey, nil -} - -func CoalesceToString(value any) string { - switch v := value.(type) { - case []any: - log.Debug().Msg("Coalescing []any to string") - strs := make([]string, 0, len(v)) - for _, item := range v { - if str, ok := item.(string); ok { - strs = append(strs, str) - continue - } - log.Warn().Interface("item", item).Msg("Item in []any is not a string, skipping") - } - return strings.Join(strs, ",") - case string: - return v - default: - log.Warn().Interface("value", value).Interface("type", v).Msg("Unsupported type, returning empty string") - return "" - } -} diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go deleted file mode 100644 index 5ae7e897..00000000 --- a/internal/utils/utils_test.go +++ /dev/null @@ -1,548 +0,0 @@ -package utils_test - -import ( - "fmt" - "os" - "reflect" - "testing" - "tinyauth/internal/types" - "tinyauth/internal/utils" -) - -func TestParseUsers(t *testing.T) { - t.Log("Testing parse users with a valid string") - - users := "user1:pass1,user2:pass2" - expected := types.Users{ - { - Username: "user1", - Password: "pass1", - }, - { - Username: "user2", - Password: "pass2", - }, - } - - result, err := utils.ParseUsers(users) - if err != nil { - t.Fatalf("Error parsing users: %v", err) - } - - if !reflect.DeepEqual(expected, result) { - t.Fatalf("Expected %v, got %v", expected, result) - } -} - -func TestGetUpperDomain(t *testing.T) { - t.Log("Testing get upper domain with a valid url") - - url := "https://sub1.sub2.domain.com:8080" - expected := "sub2.domain.com" - - result, err := utils.GetUpperDomain(url) - if err != nil { - t.Fatalf("Error getting root url: %v", err) - } - - if expected != result { - t.Fatalf("Expected %v, got %v", expected, result) - } -} - -func TestReadFile(t *testing.T) { - t.Log("Creating a test file") - - err := os.WriteFile("/tmp/test.txt", []byte("test"), 0644) - if err != nil { - t.Fatalf("Error creating test file: %v", err) - } - - t.Log("Testing read file with a valid file") - - data, err := utils.ReadFile("/tmp/test.txt") - if err != nil { - t.Fatalf("Error reading file: %v", err) - } - - if data != "test" { - t.Fatalf("Expected test, got %v", data) - } - - t.Log("Cleaning up test file") - - err = os.Remove("/tmp/test.txt") - if err != nil { - t.Fatalf("Error cleaning up test file: %v", err) - } -} - -func TestParseFileToLine(t *testing.T) { - t.Log("Testing parse file to line with a valid string") - - content := "\nuser1:pass1\nuser2:pass2\n" - expected := "user1:pass1,user2:pass2" - - result := utils.ParseFileToLine(content) - - if expected != result { - t.Fatalf("Expected %v, got %v", expected, result) - } -} - -func TestGetSecret(t *testing.T) { - t.Log("Testing get secret with an empty config and file") - - conf := "" - file := "/tmp/test.txt" - expected := "test" - - err := os.WriteFile(file, []byte(fmt.Sprintf("\n\n \n\n\n %s \n\n \n ", expected)), 0644) - if err != nil { - t.Fatalf("Error creating test file: %v", err) - } - - result := utils.GetSecret(conf, file) - - if result != expected { - t.Fatalf("Expected %v, got %v", expected, result) - } - - t.Log("Testing get secret with an empty file and a valid config") - - result = utils.GetSecret(expected, "") - - if result != expected { - t.Fatalf("Expected %v, got %v", expected, result) - } - - t.Log("Testing get secret with both a valid config and file") - - result = utils.GetSecret(expected, file) - - if result != expected { - t.Fatalf("Expected %v, got %v", expected, result) - } - - t.Log("Cleaning up test file") - - err = os.Remove(file) - if err != nil { - t.Fatalf("Error cleaning up test file: %v", err) - } -} - -func TestGetUsers(t *testing.T) { - t.Log("Testing get users with a config and no file") - - conf := "user1:pass1,user2:pass2" - file := "" - expected := types.Users{ - { - Username: "user1", - Password: "pass1", - }, - { - Username: "user2", - Password: "pass2", - }, - } - - result, err := utils.GetUsers(conf, file) - if err != nil { - t.Fatalf("Error getting users: %v", err) - } - - if !reflect.DeepEqual(expected, result) { - t.Fatalf("Expected %v, got %v", expected, result) - } - - t.Log("Testing get users with a file and no config") - - conf = "" - file = "/tmp/test.txt" - expected = types.Users{ - { - Username: "user1", - Password: "pass1", - }, - { - Username: "user2", - Password: "pass2", - }, - } - - err = os.WriteFile(file, []byte("user1:pass1\nuser2:pass2"), 0644) - if err != nil { - t.Fatalf("Error creating test file: %v", err) - } - - result, err = utils.GetUsers(conf, file) - if err != nil { - t.Fatalf("Error getting users: %v", err) - } - - if !reflect.DeepEqual(expected, result) { - t.Fatalf("Expected %v, got %v", expected, result) - } - - t.Log("Testing get users with both a config and file") - - conf = "user3:pass3" - expected = types.Users{ - { - Username: "user3", - Password: "pass3", - }, - { - Username: "user1", - Password: "pass1", - }, - { - Username: "user2", - Password: "pass2", - }, - } - - result, err = utils.GetUsers(conf, file) - if err != nil { - t.Fatalf("Error getting users: %v", err) - } - - if !reflect.DeepEqual(expected, result) { - t.Fatalf("Expected %v, got %v", expected, result) - } - - t.Log("Cleaning up test file") - - err = os.Remove(file) - if err != nil { - t.Fatalf("Error cleaning up test file: %v", err) - } -} - -func TestGetLabels(t *testing.T) { - t.Log("Testing get labels with a valid map") - - labels := map[string]string{ - "tinyauth.users": "user1,user2", - "tinyauth.oauth.whitelist": "/regex/", - "tinyauth.allowed": "random", - "tinyauth.headers": "X-Header=value", - "tinyauth.oauth.groups": "group1,group2", - } - - expected := types.Labels{ - Users: "user1,user2", - Allowed: "random", - Headers: []string{"X-Header=value"}, - OAuth: types.OAuthLabels{ - Whitelist: "/regex/", - Groups: "group1,group2", - }, - } - - result, err := utils.GetLabels(labels) - if err != nil { - t.Fatalf("Error getting labels: %v", err) - } - - if !reflect.DeepEqual(expected, result) { - t.Fatalf("Expected %v, got %v", expected, result) - } -} - -func TestParseUser(t *testing.T) { - t.Log("Testing parse user with a valid user") - - user := "user:pass:secret" - expected := types.User{ - Username: "user", - Password: "pass", - TotpSecret: "secret", - } - - result, err := utils.ParseUser(user) - if err != nil { - t.Fatalf("Error parsing user: %v", err) - } - - if !reflect.DeepEqual(expected, result) { - t.Fatalf("Expected %v, got %v", expected, result) - } - - t.Log("Testing parse user with an escaped user") - - user = "user:p$$ass$$:secret" - expected = types.User{ - Username: "user", - Password: "p$ass$", - TotpSecret: "secret", - } - - result, err = utils.ParseUser(user) - if err != nil { - t.Fatalf("Error parsing user: %v", err) - } - - if !reflect.DeepEqual(expected, result) { - t.Fatalf("Expected %v, got %v", expected, result) - } - - t.Log("Testing parse user with an invalid user") - - user = "user::pass" - - _, err = utils.ParseUser(user) - if err == nil { - t.Fatalf("Expected error parsing user") - } -} - -func TestCheckFilter(t *testing.T) { - t.Log("Testing check filter with a comma separated list") - - filter := "user1,user2,user3" - str := "user1" - expected := true - - result := utils.CheckFilter(filter, str) - - if result != expected { - t.Fatalf("Expected %v, got %v", expected, result) - } - - t.Log("Testing check filter with a regex filter") - - filter = "/^user[0-9]+$/" - str = "user1" - expected = true - - result = utils.CheckFilter(filter, str) - - if result != expected { - t.Fatalf("Expected %v, got %v", expected, result) - } - - t.Log("Testing check filter with an empty filter") - - filter = "" - str = "user1" - expected = true - - result = utils.CheckFilter(filter, str) - - if result != expected { - t.Fatalf("Expected %v, got %v", expected, result) - } - - t.Log("Testing check filter with an invalid regex filter") - - filter = "/^user[0-9+$/" - str = "user1" - expected = false - - result = utils.CheckFilter(filter, str) - - if result != expected { - t.Fatalf("Expected %v, got %v", expected, result) - } - - t.Log("Testing check filter with a non matching list") - - filter = "user1,user2,user3" - str = "user4" - expected = false - - result = utils.CheckFilter(filter, str) - - if result != expected { - t.Fatalf("Expected %v, got %v", expected, result) - } -} - -func TestSanitizeHeader(t *testing.T) { - t.Log("Testing sanitize header with a valid string") - - str := "X-Header=value" - expected := "X-Header=value" - - result := utils.SanitizeHeader(str) - - if result != expected { - t.Fatalf("Expected %v, got %v", expected, result) - } - - t.Log("Testing sanitize header with an invalid string") - - str = "X-Header=val\nue" - expected = "X-Header=value" - - result = utils.SanitizeHeader(str) - - if result != expected { - t.Fatalf("Expected %v, got %v", expected, result) - } -} - -func TestParseHeaders(t *testing.T) { - t.Log("Testing parse headers with a valid string") - - headers := []string{"X-Hea\x00der1=value1", "X-Header2=value\n2"} - expected := map[string]string{ - "X-Header1": "value1", - "X-Header2": "value2", - } - - result := utils.ParseHeaders(headers) - - if !reflect.DeepEqual(expected, result) { - t.Fatalf("Expected %v, got %v", expected, result) - } - - t.Log("Testing parse headers with an invalid string") - - headers = []string{"X-Header1=", "X-Header2", "=value", "X-Header3=value3"} - expected = map[string]string{"X-Header3": "value3"} - - result = utils.ParseHeaders(headers) - - if !reflect.DeepEqual(expected, result) { - t.Fatalf("Expected %v, got %v", expected, result) - } -} - -func TestParseSecretFile(t *testing.T) { - t.Log("Testing parse secret file with a valid file") - - content := "\n\n \n\n\n secret \n\n \n " - expected := "secret" - - result := utils.ParseSecretFile(content) - - if result != expected { - t.Fatalf("Expected %v, got %v", expected, result) - } -} - -func TestFilterIP(t *testing.T) { - t.Log("Testing filter IP with an IP and a valid CIDR") - - ip := "10.10.10.10" - filter := "10.10.10.0/24" - expected := true - - result, err := utils.FilterIP(filter, ip) - if err != nil { - t.Fatalf("Error filtering IP: %v", err) - } - - if result != expected { - t.Fatalf("Expected %v, got %v", expected, result) - } - - t.Log("Testing filter IP with an IP and a valid IP") - - filter = "10.10.10.10" - expected = true - - result, err = utils.FilterIP(filter, ip) - if err != nil { - t.Fatalf("Error filtering IP: %v", err) - } - - if result != expected { - t.Fatalf("Expected %v, got %v", expected, result) - } - - t.Log("Testing filter IP with an IP and an non matching CIDR") - - filter = "10.10.15.0/24" - expected = false - - result, err = utils.FilterIP(filter, ip) - if err != nil { - t.Fatalf("Error filtering IP: %v", err) - } - - if result != expected { - t.Fatalf("Expected %v, got %v", expected, result) - } - - t.Log("Testing filter IP with a non matching IP and a valid CIDR") - - filter = "10.10.10.11" - expected = false - - result, err = utils.FilterIP(filter, ip) - - if err != nil { - t.Fatalf("Error filtering IP: %v", err) - } - - if result != expected { - t.Fatalf("Expected %v, got %v", expected, result) - } - - t.Log("Testing filter IP with an IP and an invalid CIDR") - - filter = "10.../83" - - _, err = utils.FilterIP(filter, ip) - if err == nil { - t.Fatalf("Expected error filtering IP") - } -} - -func TestDeriveKey(t *testing.T) { - t.Log("Testing the derive key function") - - master := "master" - info := "info" - expected := "gdrdU/fXzclYjiSXRexEatVgV13qQmKl" - - result, err := utils.DeriveKey(master, info) - - if err != nil { - t.Fatalf("Error deriving key: %v", err) - } - - if result != expected { - t.Fatalf("Expected %v, got %v", expected, result) - } -} - -func TestCoalesceToString(t *testing.T) { - t.Log("Testing coalesce to string with a string") - - value := any("test") - expected := "test" - - result := utils.CoalesceToString(value) - - if result != expected { - t.Fatalf("Expected %v, got %v", expected, result) - } - - t.Log("Testing coalesce to string with a slice of strings") - - value = []any{any("test1"), any("test2"), any(123)} - expected = "test1,test2" - - result = utils.CoalesceToString(value) - - if result != expected { - t.Fatalf("Expected %v, got %v", expected, result) - } - - t.Log("Testing coalesce to string with an unsupported type") - - value = 12345 - expected = "" - - result = utils.CoalesceToString(value) - - if result != expected { - t.Fatalf("Expected %v, got %v", expected, result) - } -} diff --git a/main.go b/main.go index 27792d81..8126e9ed 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,6 @@ import ( ) func main() { - log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Logger().Level(zerolog.FatalLevel) + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Caller().Logger() cmd.Execute() } From 87ca77d74cf6b199614aa73a1427102f7a2cba7f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 18:09:02 +0300 Subject: [PATCH 002/127] chore(deps): bump github.com/go-viper/mapstructure/v2 (#322) Bumps [github.com/go-viper/mapstructure/v2](https://github.com/go-viper/mapstructure) from 2.3.0 to 2.4.0. - [Release notes](https://github.com/go-viper/mapstructure/releases) - [Changelog](https://github.com/go-viper/mapstructure/blob/main/CHANGELOG.md) - [Commits](https://github.com/go-viper/mapstructure/compare/v2.3.0...v2.4.0) --- updated-dependencies: - dependency-name: github.com/go-viper/mapstructure/v2 dependency-version: 2.4.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 +--- go.sum | 8 ++------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 8388b2a3..293dbc55 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect - github.com/go-viper/mapstructure/v2 v2.3.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/morikuni/aec v1.0.0 // indirect @@ -68,7 +68,6 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator v9.31.0+incompatible github.com/goccy/go-json v0.10.4 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect @@ -78,7 +77,6 @@ require ( github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/magiconair/properties v1.8.10 github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect diff --git a/go.sum b/go.sum index b43990cb..68edad67 100644 --- a/go.sum +++ b/go.sum @@ -111,12 +111,10 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp4Mit+3FDh548oRqwVgNsHA= -github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig= github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= -github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= -github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= 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= @@ -171,8 +169,6 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= -github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= From 03d06cb0a7c0538df92a9dce271ceee0cefa2c90 Mon Sep 17 00:00:00 2001 From: Stavros Date: Fri, 29 Aug 2025 12:35:11 +0300 Subject: [PATCH 003/127] feat: add sqlite database for storing sessions (#326) * feat: add sqlite database for storing sessions * refactor: use db instance instead of service in auth service * fix: coderabbit suggestions --- .env.example | 6 +- Dockerfile | 2 + cmd/root.go | 4 +- docker-compose.dev.yml | 1 + docker-compose.example.yml | 3 +- go.mod | 20 ++- go.sum | 74 ++++++-- internal/assets/assets.go | 7 +- .../migrations/000001_init_sqlite.down.sql | 1 + .../migrations/000001_init_sqlite.up.sql | 10 ++ internal/bootstrap/app_bootstrap.go | 33 ++-- internal/config/config.go | 4 +- internal/controller/oauth_controller.go | 14 +- internal/controller/proxy_controller.go | 8 +- internal/middleware/context_middleware.go | 2 +- internal/middleware/zerolog_middleware.go | 22 ++- internal/model/session_model.go | 12 ++ internal/service/auth_service.go | 165 +++++++----------- internal/service/database_service.go | 78 +++++++++ internal/utils/security_utils.go | 22 --- 20 files changed, 309 insertions(+), 179 deletions(-) create mode 100644 internal/assets/migrations/000001_init_sqlite.down.sql create mode 100644 internal/assets/migrations/000001_init_sqlite.up.sql create mode 100644 internal/model/session_model.go create mode 100644 internal/service/database_service.go diff --git a/.env.example b/.env.example index 0f43bf04..63cececa 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,5 @@ PORT=3000 ADDRESS=0.0.0.0 -SECRET=app_secret -SECRET_FILE=app_secret_file APP_URL=http://localhost:3000 USERS=your_user_password_hash USERS_FILE=users_file @@ -30,4 +28,6 @@ APP_TITLE=Tinyauth SSO FORGOT_PASSWORD_MESSAGE=Some message about resetting the password OAUTH_AUTO_REDIRECT=none BACKGROUND_IMAGE=some_image_url -GENERIC_SKIP_SSL=false \ No newline at end of file +GENERIC_SKIP_SSL=false +RESOURCES_DIR=/data/resources +DATABASE_PATH=/data/tinyauth.db \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 29a68b96..bd518bf2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,4 +51,6 @@ COPY --from=builder /tinyauth/tinyauth ./ EXPOSE 3000 +VOLUME ["/data"] + ENTRYPOINT ["./tinyauth"] \ No newline at end of file diff --git a/cmd/root.go b/cmd/root.go index ef5733e1..3ae72925 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -28,7 +28,6 @@ var rootCmd = &cobra.Command{ } // Check if secrets have a file associated with them - conf.Secret = utils.GetSecret(conf.Secret, conf.SecretFile) conf.GithubClientSecret = utils.GetSecret(conf.GithubClientSecret, conf.GithubClientSecretFile) conf.GoogleClientSecret = utils.GetSecret(conf.GoogleClientSecret, conf.GoogleClientSecretFile) conf.GenericClientSecret = utils.GetSecret(conf.GenericClientSecret, conf.GenericClientSecretFile) @@ -77,8 +76,6 @@ func init() { }{ {"port", 3000, "Port to run the server on."}, {"address", "0.0.0.0", "Address to bind the server to."}, - {"secret", "", "Secret to use for the cookie."}, - {"secret-file", "", "Path to a file containing the secret."}, {"app-url", "", "The Tinyauth URL."}, {"users", "", "Comma separated list of users in the format username:hash."}, {"users-file", "", "Path to a file containing users in the format username:hash."}, @@ -115,6 +112,7 @@ func init() { {"ldap-insecure", false, "Skip certificate verification for the LDAP server."}, {"ldap-search-filter", "(uid=%s)", "LDAP search filter for user lookup."}, {"resources-dir", "/data/resources", "Path to a directory containing custom resources (e.g. background image)."}, + {"database-path", "/data/tinyauth.db", "Path to the Sqlite database file."}, } for _, opt := range configOptions { diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index d85d5e31..3cf837c7 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -40,6 +40,7 @@ services: - ./cmd:/tinyauth/cmd - ./main.go:/tinyauth/main.go - /var/run/docker.sock:/var/run/docker.sock + - ./data:/data ports: - 3000:3000 - 4000:4000 diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 4b387070..9cec4a50 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -20,9 +20,10 @@ services: container_name: tinyauth image: ghcr.io/steveiliop56/tinyauth:v3 environment: - - SECRET=some-random-32-chars-string - APP_URL=https://tinyauth.example.com - USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password + volumes: + - ./data:/data labels: traefik.enable: true traefik.http.routers.tinyauth.rule: Host(`tinyauth.example.com`) diff --git a/go.mod b/go.mod index 293dbc55..cea16ddd 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 github.com/gin-gonic/gin v1.10.1 github.com/go-playground/validator/v10 v10.27.0 + github.com/golang-migrate/migrate/v4 v4.18.3 github.com/google/go-querystring v1.1.0 github.com/google/uuid v1.6.0 github.com/mdp/qrterminal/v3 v3.2.1 @@ -14,6 +15,9 @@ require ( github.com/spf13/viper v1.20.1 github.com/traefik/paerser v0.2.2 golang.org/x/crypto v0.41.0 + gorm.io/driver/sqlite v1.6.0 + gorm.io/gorm v1.30.1 + modernc.org/sqlite v1.38.2 ) require ( @@ -23,22 +27,34 @@ require ( github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/glebarez/sqlite v1.11.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/mattn/go-sqlite3 v1.14.32 // indirect github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect - github.com/morikuni/aec v1.0.0 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect go.opentelemetry.io/otel/sdk v1.34.0 // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/term v0.34.0 // indirect gotest.tools/v3 v3.5.2 // indirect + modernc.org/libc v1.66.3 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect rsc.io/qr v0.2.0 // indirect ) require ( - github.com/Microsoft/go-winio v0.4.14 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/boombuler/barcode v1.0.2 // indirect diff --git a/go.sum b/go.sum index 68edad67..0b0aceb6 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= -github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= -github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= @@ -96,6 +96,10 @@ 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-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU= @@ -120,6 +124,8 @@ github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PU 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/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs= +github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -128,6 +134,8 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= @@ -136,6 +144,11 @@ github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzq github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -152,6 +165,10 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6 github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 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/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -160,13 +177,14 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 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/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/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -180,6 +198,8 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4= github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= @@ -205,19 +225,22 @@ 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/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 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/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -229,7 +252,6 @@ github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= @@ -245,11 +267,9 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= 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.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -299,10 +319,12 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= 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/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= 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= @@ -316,10 +338,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -339,6 +359,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm 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/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= 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= @@ -358,8 +380,38 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4= +gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= +modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= +modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= +modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM= +modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= +modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek= +modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= diff --git a/internal/assets/assets.go b/internal/assets/assets.go index df6e61f1..412403c9 100644 --- a/internal/assets/assets.go +++ b/internal/assets/assets.go @@ -4,7 +4,12 @@ import ( "embed" ) -// Frontend assets +// Frontend // //go:embed dist var FrontendAssets embed.FS + +// Migrations +// +//go:embed migrations/*.sql +var Migrations embed.FS diff --git a/internal/assets/migrations/000001_init_sqlite.down.sql b/internal/assets/migrations/000001_init_sqlite.down.sql new file mode 100644 index 00000000..9a8955bd --- /dev/null +++ b/internal/assets/migrations/000001_init_sqlite.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS "sessions"; \ No newline at end of file diff --git a/internal/assets/migrations/000001_init_sqlite.up.sql b/internal/assets/migrations/000001_init_sqlite.up.sql new file mode 100644 index 00000000..4ffa992d --- /dev/null +++ b/internal/assets/migrations/000001_init_sqlite.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS "sessions" ( + "uuid" TEXT NOT NULL PRIMARY KEY UNIQUE, + "username" TEXT NOT NULL, + "email" TEXT NOT NULL, + "name" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "totp_pending" BOOLEAN NOT NULL, + "oauth_groups" TEXT NULL, + "expiry" INTEGER NOT NULL +); \ No newline at end of file diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index 594c575f..7df88efc 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -57,19 +57,6 @@ func (app *BootstrapApp) Setup() error { csrfCookieName := fmt.Sprintf("%s-%s", config.CSRFCookieName, cookieId) redirectCookieName := fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId) - // Secrets - encryptionSecret, err := utils.DeriveKey(app.Config.Secret, "encryption") - - if err != nil { - return err - } - - hmacSecret, err := utils.DeriveKey(app.Config.Secret, "hmac") - - if err != nil { - return err - } - // Create configs authConfig := service.AuthServiceConfig{ Users: users, @@ -80,8 +67,6 @@ func (app *BootstrapApp) Setup() error { LoginTimeout: app.Config.LoginTimeout, LoginMaxRetries: app.Config.LoginMaxRetries, SessionCookieName: sessionCookieName, - HMACSecret: hmacSecret, - EncryptionSecret: encryptionSecret, } // Setup services @@ -107,8 +92,24 @@ func (app *BootstrapApp) Setup() error { } } + // Bootstrap database + databaseService := service.NewDatabaseService(service.DatabaseServiceConfig{ + DatabasePath: app.Config.DatabasePath, + }) + + log.Debug().Str("service", fmt.Sprintf("%T", databaseService)).Msg("Initializing service") + + err = databaseService.Init() + + if err != nil { + return fmt.Errorf("failed to initialize database service: %w", err) + } + + database := databaseService.GetDatabase() + + // Create services dockerService := service.NewDockerService() - authService := service.NewAuthService(authConfig, dockerService, ldapService) + authService := service.NewAuthService(authConfig, dockerService, ldapService, database) oauthBrokerService := service.NewOAuthBrokerService(app.getOAuthBrokerConfig()) // Initialize services diff --git a/internal/config/config.go b/internal/config/config.go index 5d4dba86..e053f653 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -18,8 +18,6 @@ var RedirectCookieName = "tinyauth-redirect" type Config struct { Port int `mapstructure:"port" validate:"required"` Address string `validate:"required,ip4_addr" mapstructure:"address"` - Secret string `validate:"required,len=32" mapstructure:"secret"` - SecretFile string `mapstructure:"secret-file"` AppURL string `validate:"required,url" mapstructure:"app-url"` Users string `mapstructure:"users"` UsersFile string `mapstructure:"users-file"` @@ -56,6 +54,7 @@ type Config struct { LdapInsecure bool `mapstructure:"ldap-insecure"` LdapSearchFilter string `mapstructure:"ldap-search-filter"` ResourcesDir string `mapstructure:"resources-dir"` + DatabasePath string `mapstructure:"database-path" validate:"required"` } type OAuthLabels struct { @@ -112,6 +111,7 @@ type UserSearch struct { } type SessionCookie struct { + UUID string Username string Name string Email string diff --git a/internal/controller/oauth_controller.go b/internal/controller/oauth_controller.go index aa3289bb..31b21f09 100644 --- a/internal/controller/oauth_controller.go +++ b/internal/controller/oauth_controller.go @@ -144,7 +144,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { return } - if !controller.Auth.EmailWhitelisted(user.Email) { + if !controller.Auth.IsEmailWhitelisted(user.Email) { queries, err := query.Values(config.UnauthorizedQuery{ Username: user.Email, }) @@ -169,8 +169,18 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { name = fmt.Sprintf("%s (%s)", utils.Capitalize(strings.Split(user.Email, "@")[0]), strings.Split(user.Email, "@")[1]) } + var usename string + + if user.PreferredUsername != "" { + log.Debug().Msg("Using preferred username from OAuth provider") + usename = user.PreferredUsername + } else { + log.Debug().Msg("No preferred username from OAuth provider, using pseudo username") + usename = strings.Replace(user.Email, "@", "_", -1) + } + controller.Auth.CreateSessionCookie(c, &config.SessionCookie{ - Username: user.Email, + Username: usename, Name: name, Email: user.Email, Provider: req.Provider, diff --git a/internal/controller/proxy_controller.go b/internal/controller/proxy_controller.go index 348be65b..6e207e85 100644 --- a/internal/controller/proxy_controller.go +++ b/internal/controller/proxy_controller.go @@ -89,7 +89,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { clientIP := c.ClientIP() - if controller.Auth.BypassedIP(labels, clientIP) { + if controller.Auth.IsBypassedIP(labels, clientIP) { c.Header("Authorization", c.Request.Header.Get("Authorization")) headers := utils.ParseHeaders(labels.Headers) @@ -135,7 +135,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { return } - authEnabled, err := controller.Auth.AuthEnabled(uri, labels) + authEnabled, err := controller.Auth.IsAuthEnabled(uri, labels) if err != nil { log.Error().Err(err).Msg("Failed to check if auth is enabled for resource") @@ -195,7 +195,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { } if userContext.IsLoggedIn { - appAllowed := controller.Auth.ResourceAllowed(c, userContext, labels) + appAllowed := controller.Auth.IsResourceAllowed(c, userContext, labels) if !appAllowed { log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource") @@ -229,7 +229,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { } if userContext.OAuth { - groupOK := controller.Auth.OAuthGroup(c, userContext, labels) + groupOK := controller.Auth.IsInOAuthGroup(c, userContext, labels) if !groupOK { log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements") diff --git a/internal/middleware/context_middleware.go b/internal/middleware/context_middleware.go index 58e53e15..ee8932a2 100644 --- a/internal/middleware/context_middleware.go +++ b/internal/middleware/context_middleware.go @@ -83,7 +83,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { goto basic } - if !m.Auth.EmailWhitelisted(cookie.Email) { + if !m.Auth.IsEmailWhitelisted(cookie.Email) { log.Debug().Msg("Email from session cookie not whitelisted") m.Auth.DeleteSessionCookie(c) goto basic diff --git a/internal/middleware/zerolog_middleware.go b/internal/middleware/zerolog_middleware.go index 877ad4c8..f3ca4855 100644 --- a/internal/middleware/zerolog_middleware.go +++ b/internal/middleware/zerolog_middleware.go @@ -49,18 +49,24 @@ func (m *ZerologMiddleware) Middleware() gin.HandlerFunc { latency := time.Since(tStart).String() - // logPath check if the path should be logged normally or with debug + subLogger := log.With().Str("method", method). + Str("path", path). + Str("address", address). + Str("client_ip", clientIP). + Int("status", code). + Str("latency", latency).Logger() + if m.logPath(method + " " + path) { switch { - case code >= 200 && code < 300: - log.Info().Str("method", method).Str("path", path).Str("address", address).Str("clientIp", clientIP).Int("status", code).Str("latency", latency).Msg("Request") - case code >= 300 && code < 400: - log.Warn().Str("method", method).Str("path", path).Str("address", address).Str("clientIp", clientIP).Int("status", code).Str("latency", latency).Msg("Request") - case code >= 400: - log.Error().Str("method", method).Str("path", path).Str("address", address).Str("clientIp", clientIP).Int("status", code).Str("latency", latency).Msg("Request") + case code >= 400 && code < 500: + subLogger.Warn().Msg("Client Error") + case code >= 500: + subLogger.Error().Msg("Server Error") + default: + subLogger.Info().Msg("Request") } } else { - log.Debug().Str("method", method).Str("path", path).Str("address", address).Int("status", code).Str("latency", latency).Msg("Request") + subLogger.Debug().Msg("Request") } } } diff --git a/internal/model/session_model.go b/internal/model/session_model.go new file mode 100644 index 00000000..45e60659 --- /dev/null +++ b/internal/model/session_model.go @@ -0,0 +1,12 @@ +package model + +type Session struct { + UUID string `gorm:"column:uuid;primaryKey"` + Username string `gorm:"column:username"` + Email string `gorm:"column:email"` + Name string `gorm:"column:name"` + Provider string `gorm:"column:provider"` + TOTPPending bool `gorm:"column:totp_pending"` + OAuthGroups string `gorm:"column:oauth_groups"` + Expiry int64 `gorm:"column:expiry"` +} diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index 10d49e79..f55961c4 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -7,12 +7,14 @@ import ( "sync" "time" "tinyauth/internal/config" + "tinyauth/internal/model" "tinyauth/internal/utils" "github.com/gin-gonic/gin" - "github.com/gorilla/sessions" + "github.com/google/uuid" "github.com/rs/zerolog/log" "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" ) type LoginAttempt struct { @@ -30,8 +32,6 @@ type AuthServiceConfig struct { LoginTimeout int LoginMaxRetries int SessionCookieName string - HMACSecret string - EncryptionSecret string } type AuthService struct { @@ -39,49 +39,24 @@ type AuthService struct { Docker *DockerService LoginAttempts map[string]*LoginAttempt LoginMutex sync.RWMutex - Store *sessions.CookieStore LDAP *LdapService + Database *gorm.DB } -func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService) *AuthService { +func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, database *gorm.DB) *AuthService { return &AuthService{ Config: config, Docker: docker, LoginAttempts: make(map[string]*LoginAttempt), LDAP: ldap, + Database: database, } } func (auth *AuthService) Init() error { - store := sessions.NewCookieStore([]byte(auth.Config.HMACSecret), []byte(auth.Config.EncryptionSecret)) - store.Options = &sessions.Options{ - Path: "/", - MaxAge: auth.Config.SessionExpiry, - Secure: auth.Config.SecureCookie, - HttpOnly: true, - Domain: fmt.Sprintf(".%s", auth.Config.Domain), - } - - auth.Store = store return nil } -func (auth *AuthService) GetSession(c *gin.Context) (*sessions.Session, error) { - session, err := auth.Store.Get(c.Request, auth.Config.SessionCookieName) - - // If there was an error getting the session, it might be invalid so let's clear it and retry - if err != nil { - log.Debug().Err(err).Msg("Error getting session, creating a new one") - c.SetCookie(auth.Config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.Config.Domain), auth.Config.SecureCookie, true) - session, err = auth.Store.New(c.Request, auth.Config.SessionCookieName) - if err != nil { - return nil, err - } - } - - return session, nil -} - func (auth *AuthService) SearchUser(username string) config.UserSearch { if auth.GetLocalUser(username).Username != "" { return config.UserSearch{ @@ -158,30 +133,24 @@ func (auth *AuthService) IsAccountLocked(identifier string) (bool, int) { auth.LoginMutex.RLock() defer auth.LoginMutex.RUnlock() - // Return false if rate limiting is not configured if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 { return false, 0 } - // Check if the identifier exists in the map attempt, exists := auth.LoginAttempts[identifier] if !exists { return false, 0 } - // If account is locked, check if lock time has expired if attempt.LockedUntil.After(time.Now()) { - // Calculate remaining lockout time in seconds remaining := int(time.Until(attempt.LockedUntil).Seconds()) return true, remaining } - // Lock has expired return false, 0 } func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) { - // Skip if rate limiting is not configured if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 { return } @@ -189,133 +158,132 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) { auth.LoginMutex.Lock() defer auth.LoginMutex.Unlock() - // Get current attempt record or create a new one attempt, exists := auth.LoginAttempts[identifier] if !exists { attempt = &LoginAttempt{} auth.LoginAttempts[identifier] = attempt } - // Update last attempt time attempt.LastAttempt = time.Now() - // If successful login, reset failed attempts if success { attempt.FailedAttempts = 0 attempt.LockedUntil = time.Time{} // Reset lock time return } - // Increment failed attempts attempt.FailedAttempts++ - // If max retries reached, lock the account if attempt.FailedAttempts >= auth.Config.LoginMaxRetries { attempt.LockedUntil = time.Now().Add(time.Duration(auth.Config.LoginTimeout) * time.Second) log.Warn().Str("identifier", identifier).Int("timeout", auth.Config.LoginTimeout).Msg("Account locked due to too many failed login attempts") } } -func (auth *AuthService) EmailWhitelisted(email string) bool { +func (auth *AuthService) IsEmailWhitelisted(email string) bool { return utils.CheckFilter(auth.Config.OauthWhitelist, email) } func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.SessionCookie) error { - session, err := auth.GetSession(c) + uuid, err := uuid.NewRandom() + if err != nil { return err } - var sessionExpiry int + var expiry int if data.TotpPending { - sessionExpiry = 3600 + expiry = 3600 } else { - sessionExpiry = auth.Config.SessionExpiry + expiry = auth.Config.SessionExpiry + } + + session := model.Session{ + UUID: uuid.String(), + Username: data.Username, + Email: data.Email, + Name: data.Name, + Provider: data.Provider, + TOTPPending: data.TotpPending, + OAuthGroups: data.OAuthGroups, + Expiry: time.Now().Add(time.Duration(expiry) * time.Second).Unix(), } - session.Values["username"] = data.Username - session.Values["name"] = data.Name - session.Values["email"] = data.Email - session.Values["provider"] = data.Provider - session.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix() - session.Values["totpPending"] = data.TotpPending - session.Values["oauthGroups"] = data.OAuthGroups + err = auth.Database.Create(&session).Error - err = session.Save(c.Request, c.Writer) if err != nil { return err } + c.SetCookie(auth.Config.SessionCookieName, session.UUID, expiry, "/", fmt.Sprintf(".%s", auth.Config.Domain), auth.Config.SecureCookie, true) + return nil } func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error { - session, err := auth.GetSession(c) + cookie, err := c.Cookie(auth.Config.SessionCookieName) + if err != nil { return err } - // Delete all values in the session - for key := range session.Values { - delete(session.Values, key) - } + res := auth.Database.Unscoped().Where("uuid = ?", cookie).Delete(&model.Session{}) - err = session.Save(c.Request, c.Writer) - if err != nil { - return err + if res.Error != nil { + return res.Error } - // Clear the cookie in the browser c.SetCookie(auth.Config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.Config.Domain), auth.Config.SecureCookie, true) return nil } func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie, error) { - session, err := auth.GetSession(c) + cookie, err := c.Cookie(auth.Config.SessionCookieName) + if err != nil { return config.SessionCookie{}, err } - username, usernameOk := session.Values["username"].(string) - email, emailOk := session.Values["email"].(string) - name, nameOk := session.Values["name"].(string) - provider, providerOK := session.Values["provider"].(string) - expiry, expiryOk := session.Values["expiry"].(int64) - totpPending, totpPendingOk := session.Values["totpPending"].(bool) - oauthGroups, oauthGroupsOk := session.Values["oauthGroups"].(string) + var session model.Session + + res := auth.Database.Unscoped().Where("uuid = ?", cookie).First(&session) + + if res.Error != nil { + return config.SessionCookie{}, res.Error + } - // If any data is missing, delete the session cookie - if !usernameOk || !providerOK || !expiryOk || !totpPendingOk || !emailOk || !nameOk || !oauthGroupsOk { - log.Warn().Msg("Session cookie is invalid") - auth.DeleteSessionCookie(c) - return config.SessionCookie{}, nil + if res.RowsAffected == 0 { + return config.SessionCookie{}, fmt.Errorf("session not found") } - // If the session cookie has expired, delete it - if time.Now().Unix() > expiry { - log.Warn().Msg("Session cookie expired") - auth.DeleteSessionCookie(c) - return config.SessionCookie{}, nil + currentTime := time.Now().Unix() + + if currentTime > session.Expiry { + res := auth.Database.Unscoped().Where("uuid = ?", session.UUID).Delete(&model.Session{}) + if res.Error != nil { + log.Error().Err(res.Error).Msg("Failed to delete expired session") + } + return config.SessionCookie{}, fmt.Errorf("session expired") } return config.SessionCookie{ - Username: username, - Name: name, - Email: email, - Provider: provider, - TotpPending: totpPending, - OAuthGroups: oauthGroups, + UUID: session.UUID, + Username: session.Username, + Email: session.Email, + Name: session.Name, + Provider: session.Provider, + TotpPending: session.TOTPPending, + OAuthGroups: session.OAuthGroups, }, nil } func (auth *AuthService) UserAuthConfigured() bool { - // If there are users or LDAP is configured, return true return len(auth.Config.Users) > 0 || auth.LDAP != nil } -func (auth *AuthService) ResourceAllowed(c *gin.Context, context config.UserContext, labels config.Labels) bool { +func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, labels config.Labels) bool { if context.OAuth { log.Debug().Msg("Checking OAuth whitelist") return utils.CheckFilter(labels.OAuth.Whitelist, context.Email) @@ -325,7 +293,7 @@ func (auth *AuthService) ResourceAllowed(c *gin.Context, context config.UserCont return utils.CheckFilter(labels.Users, context.Username) } -func (auth *AuthService) OAuthGroup(c *gin.Context, context config.UserContext, labels config.Labels) bool { +func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserContext, labels config.Labels) bool { if labels.OAuth.Groups == "" { return true } @@ -335,23 +303,20 @@ func (auth *AuthService) OAuthGroup(c *gin.Context, context config.UserContext, return true } - // Split the groups by comma (no need to parse since they are from the API response) + // No need to parse since they are from the API response oauthGroups := strings.Split(context.OAuthGroups, ",") - // For every group check if it is in the required groups for _, group := range oauthGroups { if utils.CheckFilter(labels.OAuth.Groups, group) { return true } } - // No groups matched log.Debug().Msg("No groups matched") return false } -func (auth *AuthService) AuthEnabled(uri string, labels config.Labels) (bool, error) { - // If the label is empty, auth is enabled +func (auth *AuthService) IsAuthEnabled(uri string, labels config.Labels) (bool, error) { if labels.Allowed == "" { return true, nil } @@ -362,12 +327,10 @@ func (auth *AuthService) AuthEnabled(uri string, labels config.Labels) (bool, er return true, err } - // If the regex matches the URI, auth is not enabled if regex.MatchString(uri) { return false, nil } - // Auth enabled return true, nil } @@ -384,7 +347,6 @@ func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User { } func (auth *AuthService) CheckIP(labels config.Labels, ip string) bool { - // Check if the IP is in block list for _, blocked := range labels.IP.Block { res, err := utils.FilterIP(blocked, ip) if err != nil { @@ -397,7 +359,6 @@ func (auth *AuthService) CheckIP(labels config.Labels, ip string) bool { } } - // For every IP in the allow list, check if the IP matches for _, allowed := range labels.IP.Allow { res, err := utils.FilterIP(allowed, ip) if err != nil { @@ -410,7 +371,6 @@ func (auth *AuthService) CheckIP(labels config.Labels, ip string) bool { } } - // If not in allowed range and allowed range is not empty, deny access if len(labels.IP.Allow) > 0 { log.Debug().Str("ip", ip).Msg("IP not in allow list, denying access") return false @@ -420,8 +380,7 @@ func (auth *AuthService) CheckIP(labels config.Labels, ip string) bool { return true } -func (auth *AuthService) BypassedIP(labels config.Labels, ip string) bool { - // For every IP in the bypass list, check if the IP matches +func (auth *AuthService) IsBypassedIP(labels config.Labels, ip string) bool { for _, bypassed := range labels.IP.Bypass { res, err := utils.FilterIP(bypassed, ip) if err != nil { diff --git a/internal/service/database_service.go b/internal/service/database_service.go new file mode 100644 index 00000000..858ba4cc --- /dev/null +++ b/internal/service/database_service.go @@ -0,0 +1,78 @@ +package service + +import ( + "database/sql" + "tinyauth/internal/assets" + + "github.com/glebarez/sqlite" + "github.com/golang-migrate/migrate/v4" + sqliteMigrate "github.com/golang-migrate/migrate/v4/database/sqlite3" + "github.com/golang-migrate/migrate/v4/source/iofs" + "gorm.io/gorm" +) + +type DatabaseServiceConfig struct { + DatabasePath string +} + +type DatabaseService struct { + Config DatabaseServiceConfig + Database *gorm.DB +} + +func NewDatabaseService(config DatabaseServiceConfig) *DatabaseService { + return &DatabaseService{ + Config: config, + } +} + +func (ds *DatabaseService) Init() error { + gormDB, err := gorm.Open(sqlite.Open(ds.Config.DatabasePath), &gorm.Config{}) + + if err != nil { + return err + } + + sqlDB, err := gormDB.DB() + + if err != nil { + return err + } + + sqlDB.SetMaxOpenConns(1) + + err = ds.migrateDatabase(sqlDB) + + if err != nil && err != migrate.ErrNoChange { + return err + } + + ds.Database = gormDB + return nil +} + +func (ds *DatabaseService) migrateDatabase(sqlDB *sql.DB) error { + data, err := iofs.New(assets.Migrations, "migrations") + + if err != nil { + return err + } + + target, err := sqliteMigrate.WithInstance(sqlDB, &sqliteMigrate.Config{}) + + if err != nil { + return err + } + + migrator, err := migrate.NewWithInstance("iofs", data, "tinyauth", target) + + if err != nil { + return err + } + + return migrator.Up() +} + +func (ds *DatabaseService) GetDatabase() *gorm.DB { + return ds.Database +} diff --git a/internal/utils/security_utils.go b/internal/utils/security_utils.go index a0319008..b40c56ca 100644 --- a/internal/utils/security_utils.go +++ b/internal/utils/security_utils.go @@ -1,17 +1,13 @@ package utils import ( - "bytes" - "crypto/sha256" "encoding/base64" "errors" - "io" "net" "regexp" "strings" "github.com/google/uuid" - "golang.org/x/crypto/hkdf" ) func GetSecret(conf string, file string) string { @@ -49,24 +45,6 @@ func GetBasicAuth(username string, password string) string { return base64.StdEncoding.EncodeToString([]byte(auth)) } -func DeriveKey(secret string, info string) (string, error) { - hash := sha256.New - hkdf := hkdf.New(hash, []byte(secret), nil, []byte(info)) // I am not using a salt because I just want two different keys from one secret, maybe bad practice - key := make([]byte, 24) - - _, err := io.ReadFull(hkdf, key) - if err != nil { - return "", err - } - - if bytes.Equal(key, make([]byte, 24)) { - return "", errors.New("derived key is empty") - } - - encodedKey := base64.StdEncoding.EncodeToString(key) - return encodedKey, nil -} - func FilterIP(filter string, ip string) (bool, error) { ipAddr := net.ParseIP(ip) From c7c3de4f785896276ca3b78b4b37aee595f5d0ad Mon Sep 17 00:00:00 2001 From: Stavros Date: Fri, 29 Aug 2025 17:04:34 +0300 Subject: [PATCH 004/127] refactor: unify labels (#329) * refactor: unify labels * feat: implement path block and user block Fixes #313 * fix: fix oauth group check logic * chore: fix typo --- internal/bootstrap/app_bootstrap.go | 2 +- internal/config/config.go | 102 ++++++++++++++++-------- internal/controller/proxy_controller.go | 81 ++++++++++--------- internal/service/auth_service.go | 64 +++++++++------ internal/service/docker_service.go | 27 +++---- internal/utils/label_utils.go | 2 +- 6 files changed, 164 insertions(+), 114 deletions(-) diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index 7df88efc..af75aa6d 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -181,7 +181,7 @@ func (app *BootstrapApp) Setup() error { Title: app.Config.Title, GenericName: app.Config.GenericName, Domain: domain, - ForgotPasswordMessage: app.Config.FogotPasswordMessage, + ForgotPasswordMessage: app.Config.ForgotPasswordMessage, BackgroundImage: app.Config.BackgroundImage, OAuthAutoRedirect: app.Config.OAuthAutoRedirect, }, apiRouter) diff --git a/internal/config/config.go b/internal/config/config.go index e053f653..c959e265 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,20 +1,19 @@ package config -type Claims struct { - Name string `json:"name"` - Email string `json:"email"` - PreferredUsername string `json:"preferred_username"` - Groups any `json:"groups"` -} +// Version information, set at build time var Version = "development" var CommitHash = "n/a" var BuildTimestamp = "n/a" +// Cookie name templates + var SessionCookieName = "tinyauth-session" var CSRFCookieName = "tinyauth-csrf" var RedirectCookieName = "tinyauth-redirect" +// Main app config + type Config struct { Port int `mapstructure:"port" validate:"required"` Address string `validate:"required,ip4_addr" mapstructure:"address"` @@ -45,7 +44,7 @@ type Config struct { Title string `mapstructure:"app-title"` LoginTimeout int `mapstructure:"login-timeout"` LoginMaxRetries int `mapstructure:"login-max-retries"` - FogotPasswordMessage string `mapstructure:"forgot-password-message"` + ForgotPasswordMessage string `mapstructure:"forgot-password-message"` BackgroundImage string `mapstructure:"background-image" validate:"required"` LdapAddress string `mapstructure:"ldap-address"` LdapBindDN string `mapstructure:"ldap-bind-dn"` @@ -57,35 +56,13 @@ type Config struct { DatabasePath string `mapstructure:"database-path" validate:"required"` } -type OAuthLabels struct { - Whitelist string - Groups string -} - -type BasicLabels struct { - Username string - Password PasswordLabels -} +// OAuth/OIDC config -type PasswordLabels struct { - Plain string - File string -} - -type IPLabels struct { - Allow []string - Block []string - Bypass []string -} - -type Labels struct { - Users string - Allowed string - Headers []string - Domain []string - Basic BasicLabels - OAuth OAuthLabels - IP IPLabels +type Claims struct { + Name string `json:"name"` + Email string `json:"email"` + PreferredUsername string `json:"preferred_username"` + Groups any `json:"groups"` } type OAuthServiceConfig struct { @@ -99,6 +76,8 @@ type OAuthServiceConfig struct { InsecureSkipVerify bool } +// User/session related stuff + type User struct { Username string Password string @@ -132,6 +111,8 @@ type UserContext struct { TotpEnabled bool } +// API responses and queries + type UnauthorizedQuery struct { Username string `url:"username"` Resource string `url:"resource"` @@ -142,3 +123,54 @@ type UnauthorizedQuery struct { type RedirectQuery struct { RedirectURI string `url:"redirect_uri"` } + +// Labels + +type Labels struct { + Apps map[string]AppLabels +} + +type AppLabels struct { + Config ConfigLabels + Users UsersLabels + OAuth OAuthLabels + IP IPLabels + Response ResponseLabels + Path PathLabels +} + +type ConfigLabels struct { + Domain string +} + +type UsersLabels struct { + Allow string + Block string +} + +type OAuthLabels struct { + Whitelist string + Groups string +} + +type IPLabels struct { + Allow []string + Block []string + Bypass []string +} + +type ResponseLabels struct { + Headers []string + BasicAuth BasicAuthLabels +} + +type BasicAuthLabels struct { + Username string + Password string + PasswordFile string +} + +type PathLabels struct { + Allow string + Block string +} diff --git a/internal/controller/proxy_controller.go b/internal/controller/proxy_controller.go index 6e207e85..6ad10eca 100644 --- a/internal/controller/proxy_controller.go +++ b/internal/controller/proxy_controller.go @@ -89,19 +89,20 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { clientIP := c.ClientIP() - if controller.Auth.IsBypassedIP(labels, clientIP) { + if controller.Auth.IsBypassedIP(labels.IP, clientIP) { c.Header("Authorization", c.Request.Header.Get("Authorization")) - headers := utils.ParseHeaders(labels.Headers) + headers := utils.ParseHeaders(labels.Response.Headers) for key, value := range headers { log.Debug().Str("header", key).Msg("Setting header") c.Header(key, value) } - if labels.Basic.Username != "" && utils.GetSecret(labels.Basic.Password.Plain, labels.Basic.Password.File) != "" { - log.Debug().Str("username", labels.Basic.Username).Msg("Setting basic auth header") - c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(labels.Basic.Username, utils.GetSecret(labels.Basic.Password.Plain, labels.Basic.Password.File)))) + basicPassword := utils.GetSecret(labels.Response.BasicAuth.Password, labels.Response.BasicAuth.PasswordFile) + if labels.Response.BasicAuth.Username != "" && basicPassword != "" { + log.Debug().Str("username", labels.Response.BasicAuth.Username).Msg("Setting basic auth header") + c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(labels.Response.BasicAuth.Username, basicPassword))) } c.JSON(200, gin.H{ @@ -111,31 +112,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { return } - if !controller.Auth.CheckIP(labels, clientIP) { - if req.Proxy == "nginx" || !isBrowser { - c.JSON(401, gin.H{ - "status": 401, - "message": "Unauthorized", - }) - return - } - - queries, err := query.Values(config.UnauthorizedQuery{ - Resource: strings.Split(host, ".")[0], - IP: clientIP, - }) - - if err != nil { - log.Error().Err(err).Msg("Failed to encode unauthorized query") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) - return - } - - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.Config.AppURL, queries.Encode())) - return - } - - authEnabled, err := controller.Auth.IsAuthEnabled(uri, labels) + authEnabled, err := controller.Auth.IsAuthEnabled(uri, labels.Path) if err != nil { log.Error().Err(err).Msg("Failed to check if auth is enabled for resource") @@ -157,16 +134,17 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { c.Header("Authorization", c.Request.Header.Get("Authorization")) - headers := utils.ParseHeaders(labels.Headers) + headers := utils.ParseHeaders(labels.Response.Headers) for key, value := range headers { log.Debug().Str("header", key).Msg("Setting header") c.Header(key, value) } - if labels.Basic.Username != "" && utils.GetSecret(labels.Basic.Password.Plain, labels.Basic.Password.File) != "" { - log.Debug().Str("username", labels.Basic.Username).Msg("Setting basic auth header") - c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(labels.Basic.Username, utils.GetSecret(labels.Basic.Password.Plain, labels.Basic.Password.File)))) + basicPassword := utils.GetSecret(labels.Response.BasicAuth.Password, labels.Response.BasicAuth.PasswordFile) + if labels.Response.BasicAuth.Username != "" && basicPassword != "" { + log.Debug().Str("username", labels.Response.BasicAuth.Username).Msg("Setting basic auth header") + c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(labels.Response.BasicAuth.Username, basicPassword))) } c.JSON(200, gin.H{ @@ -176,6 +154,30 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { return } + if !controller.Auth.CheckIP(labels.IP, clientIP) { + if req.Proxy == "nginx" || !isBrowser { + c.JSON(401, gin.H{ + "status": 401, + "message": "Unauthorized", + }) + return + } + + queries, err := query.Values(config.UnauthorizedQuery{ + Resource: strings.Split(host, ".")[0], + IP: clientIP, + }) + + if err != nil { + log.Error().Err(err).Msg("Failed to encode unauthorized query") + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) + return + } + + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.Config.AppURL, queries.Encode())) + return + } + var userContext config.UserContext context, err := utils.GetContext(c) @@ -229,7 +231,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { } if userContext.OAuth { - groupOK := controller.Auth.IsInOAuthGroup(c, userContext, labels) + groupOK := controller.Auth.IsInOAuthGroup(c, userContext, labels.OAuth.Groups) if !groupOK { log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements") @@ -270,16 +272,17 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { c.Header("Remote-Email", utils.SanitizeHeader(userContext.Email)) c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups)) - headers := utils.ParseHeaders(labels.Headers) + headers := utils.ParseHeaders(labels.Response.Headers) for key, value := range headers { log.Debug().Str("header", key).Msg("Setting header") c.Header(key, value) } - if labels.Basic.Username != "" && utils.GetSecret(labels.Basic.Password.Plain, labels.Basic.Password.File) != "" { - log.Debug().Str("username", labels.Basic.Username).Msg("Setting basic auth header") - c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(labels.Basic.Username, utils.GetSecret(labels.Basic.Password.Plain, labels.Basic.Password.File)))) + basicPassword := utils.GetSecret(labels.Response.BasicAuth.Password, labels.Response.BasicAuth.PasswordFile) + if labels.Response.BasicAuth.Username != "" && basicPassword != "" { + log.Debug().Str("username", labels.Response.BasicAuth.Username).Msg("Setting basic auth header") + c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(labels.Response.BasicAuth.Username, basicPassword))) } c.JSON(200, gin.H{ diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index f55961c4..c7a27c1f 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -283,18 +283,25 @@ func (auth *AuthService) UserAuthConfigured() bool { return len(auth.Config.Users) > 0 || auth.LDAP != nil } -func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, labels config.Labels) bool { +func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, labels config.AppLabels) bool { if context.OAuth { log.Debug().Msg("Checking OAuth whitelist") return utils.CheckFilter(labels.OAuth.Whitelist, context.Email) } + if labels.Users.Block != "" { + log.Debug().Msg("Checking blocked users") + if utils.CheckFilter(labels.Users.Block, context.Username) { + return false + } + } + log.Debug().Msg("Checking users") - return utils.CheckFilter(labels.Users, context.Username) + return utils.CheckFilter(labels.Users.Allow, context.Username) } -func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserContext, labels config.Labels) bool { - if labels.OAuth.Groups == "" { +func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserContext, requiredGroups string) bool { + if requiredGroups == "" { return true } @@ -303,11 +310,8 @@ func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserConte return true } - // No need to parse since they are from the API response - oauthGroups := strings.Split(context.OAuthGroups, ",") - - for _, group := range oauthGroups { - if utils.CheckFilter(labels.OAuth.Groups, group) { + for _, userGroup := range strings.Split(context.OAuthGroups, ",") { + if utils.CheckFilter(requiredGroups, strings.TrimSpace(userGroup)) { return true } } @@ -316,19 +320,31 @@ func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserConte return false } -func (auth *AuthService) IsAuthEnabled(uri string, labels config.Labels) (bool, error) { - if labels.Allowed == "" { - return true, nil - } +func (auth *AuthService) IsAuthEnabled(uri string, path config.PathLabels) (bool, error) { + // Check for block list + if path.Block != "" { + regex, err := regexp.Compile(path.Block) - regex, err := regexp.Compile(labels.Allowed) + if err != nil { + return true, err + } - if err != nil { - return true, err + if !regex.MatchString(uri) { + return false, nil + } } - if regex.MatchString(uri) { - return false, nil + // Check for allow list + if path.Allow != "" { + regex, err := regexp.Compile(path.Allow) + + if err != nil { + return true, err + } + + if regex.MatchString(uri) { + return false, nil + } } return true, nil @@ -346,8 +362,8 @@ func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User { } } -func (auth *AuthService) CheckIP(labels config.Labels, ip string) bool { - for _, blocked := range labels.IP.Block { +func (auth *AuthService) CheckIP(labels config.IPLabels, ip string) bool { + for _, blocked := range labels.Block { res, err := utils.FilterIP(blocked, ip) if err != nil { log.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list") @@ -359,7 +375,7 @@ func (auth *AuthService) CheckIP(labels config.Labels, ip string) bool { } } - for _, allowed := range labels.IP.Allow { + for _, allowed := range labels.Allow { res, err := utils.FilterIP(allowed, ip) if err != nil { log.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list") @@ -371,7 +387,7 @@ func (auth *AuthService) CheckIP(labels config.Labels, ip string) bool { } } - if len(labels.IP.Allow) > 0 { + if len(labels.Allow) > 0 { log.Debug().Str("ip", ip).Msg("IP not in allow list, denying access") return false } @@ -380,8 +396,8 @@ func (auth *AuthService) CheckIP(labels config.Labels, ip string) bool { return true } -func (auth *AuthService) IsBypassedIP(labels config.Labels, ip string) bool { - for _, bypassed := range labels.IP.Bypass { +func (auth *AuthService) IsBypassedIP(labels config.IPLabels, ip string) bool { + for _, bypassed := range labels.Bypass { res, err := utils.FilterIP(bypassed, ip) if err != nil { log.Warn().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list") diff --git a/internal/service/docker_service.go b/internal/service/docker_service.go index 41eb07c9..e078a7e0 100644 --- a/internal/service/docker_service.go +++ b/internal/service/docker_service.go @@ -6,8 +6,6 @@ import ( "tinyauth/internal/config" "tinyauth/internal/utils" - "slices" - container "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" "github.com/rs/zerolog/log" @@ -57,17 +55,17 @@ func (docker *DockerService) DockerConnected() bool { return err == nil } -func (docker *DockerService) GetLabels(app string, domain string) (config.Labels, error) { +func (docker *DockerService) GetLabels(app string, domain string) (config.AppLabels, error) { isConnected := docker.DockerConnected() if !isConnected { log.Debug().Msg("Docker not connected, returning empty labels") - return config.Labels{}, nil + return config.AppLabels{}, nil } containers, err := docker.GetContainers() if err != nil { - return config.Labels{}, err + return config.AppLabels{}, err } for _, container := range containers { @@ -83,18 +81,19 @@ func (docker *DockerService) GetLabels(app string, domain string) (config.Labels continue } - // Check if the container matches the ID or domain - if slices.Contains(labels.Domain, domain) { - log.Debug().Str("id", inspect.ID).Msg("Found matching container by domain") - return labels, nil - } + for appName, appLabels := range labels.Apps { + if appLabels.Config.Domain == domain { + log.Debug().Str("id", inspect.ID).Msg("Found matching container by domain") + return appLabels, nil + } - if strings.TrimPrefix(inspect.Name, "/") == app { - log.Debug().Str("id", inspect.ID).Msg("Found matching container by name") - return labels, nil + if strings.TrimPrefix(inspect.Name, "/") == appName { + log.Debug().Str("id", inspect.ID).Msg("Found matching container by app name") + return appLabels, nil + } } } log.Debug().Msg("No matching container found, returning empty labels") - return config.Labels{}, nil + return config.AppLabels{}, nil } diff --git a/internal/utils/label_utils.go b/internal/utils/label_utils.go index f10092df..5e423f7a 100644 --- a/internal/utils/label_utils.go +++ b/internal/utils/label_utils.go @@ -11,7 +11,7 @@ import ( func GetLabels(labels map[string]string) (config.Labels, error) { var labelsParsed config.Labels - err := parser.Decode(labels, &labelsParsed, "tinyauth", "tinyauth.users", "tinyauth.allowed", "tinyauth.headers", "tinyauth.domain", "tinyauth.basic", "tinyauth.oauth", "tinyauth.ip") + err := parser.Decode(labels, &labelsParsed, "tinyauth", "tinyauth.apps") if err != nil { return config.Labels{}, err } From 55e60a6ed9a005e2c44b6ac155744821a2239529 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 30 Aug 2025 11:52:25 +0300 Subject: [PATCH 005/127] chore(deps): bump oven/bun from 1.2.20-alpine to 1.2.21-alpine (#327) Bumps oven/bun from 1.2.20-alpine to 1.2.21-alpine. --- updated-dependencies: - dependency-name: oven/bun dependency-version: 1.2.21-alpine dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index bd518bf2..865dfe5e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Site builder -FROM oven/bun:1.2.20-alpine AS frontend-builder +FROM oven/bun:1.2.21-alpine AS frontend-builder WORKDIR /frontend From 17048d94b6511299d9735eeb128a17d00cac6bd2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 30 Aug 2025 11:53:06 +0300 Subject: [PATCH 006/127] chore(deps): bump the minor-patch group across 1 directory with 18 updates (#328) Bumps the minor-patch group with 18 updates in the /frontend directory: | Package | From | To | | --- | --- | --- | | [@radix-ui/react-select](https://github.com/radix-ui/primitives) | `2.2.5` | `2.2.6` | | [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.1.11` | `4.1.12` | | [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.84.1` | `5.85.5` | | [i18next](https://github.com/i18next/i18next) | `25.3.2` | `25.4.2` | | [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.539.0` | `0.541.0` | | [react-i18next](https://github.com/i18next/react-i18next) | `15.6.1` | `15.7.2` | | [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) | `7.8.0` | `7.8.2` | | [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.1.11` | `4.1.12` | | [zod](https://github.com/colinhacks/zod) | `4.0.15` | `4.1.3` | | [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) | `9.32.0` | `9.34.0` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `24.2.0` | `24.3.0` | | [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.1.9` | `19.1.11` | | [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `19.1.7` | `19.1.8` | | [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `5.0.0` | `5.0.1` | | [eslint](https://github.com/eslint/eslint) | `9.32.0` | `9.34.0` | | [tw-animate-css](https://github.com/Wombosvideo/tw-animate-css) | `1.3.6` | `1.3.7` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.39.0` | `8.41.0` | | [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.1.1` | `7.1.3` | Updates `@radix-ui/react-select` from 2.2.5 to 2.2.6 - [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md) - [Commits](https://github.com/radix-ui/primitives/commits) Updates `@tailwindcss/vite` from 4.1.11 to 4.1.12 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.12/packages/@tailwindcss-vite) Updates `@tanstack/react-query` from 5.84.1 to 5.85.5 - [Release notes](https://github.com/TanStack/query/releases) - [Commits](https://github.com/TanStack/query/commits/v5.85.5/packages/react-query) Updates `i18next` from 25.3.2 to 25.4.2 - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/i18next/compare/v25.3.2...v25.4.2) Updates `lucide-react` from 0.539.0 to 0.541.0 - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/0.541.0/packages/lucide-react) Updates `react-i18next` from 15.6.1 to 15.7.2 - [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/react-i18next/compare/v15.6.1...v15.7.2) Updates `react-router` from 7.8.0 to 7.8.2 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router@7.8.2/packages/react-router) Updates `tailwindcss` from 4.1.11 to 4.1.12 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.12/packages/tailwindcss) Updates `zod` from 4.0.15 to 4.1.3 - [Release notes](https://github.com/colinhacks/zod/releases) - [Commits](https://github.com/colinhacks/zod/compare/v4.0.15...v4.1.3) Updates `@eslint/js` from 9.32.0 to 9.34.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/commits/v9.34.0/packages/js) Updates `@types/node` from 24.2.0 to 24.3.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/react` from 19.1.9 to 19.1.11 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react) Updates `@types/react-dom` from 19.1.7 to 19.1.8 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom) Updates `@vitejs/plugin-react` from 5.0.0 to 5.0.1 - [Release notes](https://github.com/vitejs/vite-plugin-react/releases) - [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@5.0.1/packages/plugin-react) Updates `eslint` from 9.32.0 to 9.34.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v9.32.0...v9.34.0) Updates `tw-animate-css` from 1.3.6 to 1.3.7 - [Release notes](https://github.com/Wombosvideo/tw-animate-css/releases) - [Commits](https://github.com/Wombosvideo/tw-animate-css/compare/v1.3.6...v1.3.7) Updates `typescript-eslint` from 8.39.0 to 8.41.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.41.0/packages/typescript-eslint) Updates `vite` from 7.1.1 to 7.1.3 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v7.1.3/packages/vite) --- updated-dependencies: - dependency-name: "@radix-ui/react-select" dependency-version: 2.2.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: "@tailwindcss/vite" dependency-version: 4.1.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: "@tanstack/react-query" dependency-version: 5.85.5 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: i18next dependency-version: 25.4.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: lucide-react dependency-version: 0.541.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: react-i18next dependency-version: 15.7.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: react-router dependency-version: 7.8.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: tailwindcss dependency-version: 4.1.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: zod dependency-version: 4.1.3 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@eslint/js" dependency-version: 9.34.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@types/node" dependency-version: 24.3.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@types/react" dependency-version: 19.1.11 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: "@types/react-dom" dependency-version: 19.1.8 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: "@vitejs/plugin-react" dependency-version: 5.0.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: eslint dependency-version: 9.34.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: tw-animate-css dependency-version: 1.3.7 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: typescript-eslint dependency-version: 8.41.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: vite dependency-version: 7.1.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/bun.lock | 230 ++++++++++++++++++++---------------------- frontend/package.json | 36 +++---- 2 files changed, 127 insertions(+), 139 deletions(-) diff --git a/frontend/bun.lock b/frontend/bun.lock index 6b37b63b..12b197ba 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -6,48 +6,48 @@ "dependencies": { "@hookform/resolvers": "^5.2.1", "@radix-ui/react-label": "^2.1.7", - "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", - "@tailwindcss/vite": "^4.1.11", - "@tanstack/react-query": "^5.84.1", + "@tailwindcss/vite": "^4.1.12", + "@tanstack/react-query": "^5.85.5", "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dompurify": "^3.2.6", - "i18next": "^25.3.2", + "i18next": "^25.4.2", "i18next-browser-languagedetector": "^8.2.0", "i18next-resources-to-backend": "^1.2.1", "input-otp": "^1.4.2", - "lucide-react": "^0.539.0", + "lucide-react": "^0.541.0", "next-themes": "^0.4.6", "react": "^19.1.1", "react-dom": "^19.1.1", "react-hook-form": "^7.62.0", - "react-i18next": "^15.6.1", + "react-i18next": "^15.7.2", "react-markdown": "^10.1.0", - "react-router": "^7.8.0", + "react-router": "^7.8.2", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", - "tailwindcss": "^4.1.11", - "zod": "^4.0.15", + "tailwindcss": "^4.1.12", + "zod": "^4.1.3", }, "devDependencies": { - "@eslint/js": "^9.32.0", + "@eslint/js": "^9.34.0", "@tanstack/eslint-plugin-query": "^5.83.1", - "@types/node": "^24.2.0", - "@types/react": "^19.1.9", - "@types/react-dom": "^19.1.7", - "@vitejs/plugin-react": "^5.0.0", - "eslint": "^9.32.0", + "@types/node": "^24.3.0", + "@types/react": "^19.1.11", + "@types/react-dom": "^19.1.8", + "@vitejs/plugin-react": "^5.0.1", + "eslint": "^9.34.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^16.3.0", "prettier": "3.6.2", - "tw-animate-css": "^1.3.6", + "tw-animate-css": "^1.3.7", "typescript": "~5.9.2", - "typescript-eslint": "^8.39.0", - "vite": "^7.1.1", + "typescript-eslint": "^8.41.0", + "vite": "^7.1.3", }, }, }, @@ -58,9 +58,9 @@ "@babel/compat-data": ["@babel/compat-data@7.27.2", "", {}, "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ=="], - "@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], + "@babel/core": ["@babel/core@7.28.3", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.3", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ=="], - "@babel/generator": ["@babel/generator@7.28.0", "", { "dependencies": { "@babel/parser": "^7.28.0", "@babel/types": "^7.28.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg=="], + "@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="], "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], @@ -68,7 +68,7 @@ "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.27.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.27.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg=="], + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], @@ -78,9 +78,9 @@ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - "@babel/helpers": ["@babel/helpers@7.27.6", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.6" } }, "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug=="], + "@babel/helpers": ["@babel/helpers@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.2" } }, "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw=="], - "@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="], + "@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="], "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], @@ -90,9 +90,9 @@ "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], - "@babel/traverse": ["@babel/traverse@7.28.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/types": "^7.28.0", "debug": "^4.3.1" } }, "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg=="], + "@babel/traverse": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="], - "@babel/types": ["@babel/types@7.28.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ=="], + "@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], @@ -150,17 +150,17 @@ "@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="], - "@eslint/config-helpers": ["@eslint/config-helpers@0.3.0", "", {}, "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw=="], + "@eslint/config-helpers": ["@eslint/config-helpers@0.3.1", "", {}, "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA=="], - "@eslint/core": ["@eslint/core@0.15.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA=="], + "@eslint/core": ["@eslint/core@0.15.2", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg=="], "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], - "@eslint/js": ["@eslint/js@9.32.0", "", {}, "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg=="], + "@eslint/js": ["@eslint/js@9.34.0", "", {}, "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw=="], "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.4", "", { "dependencies": { "@eslint/core": "^0.15.1", "levn": "^0.4.1" } }, "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw=="], + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.5", "", { "dependencies": { "@eslint/core": "^0.15.2", "levn": "^0.4.1" } }, "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w=="], "@floating-ui/core": ["@floating-ui/core@1.7.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA=="], @@ -184,6 +184,8 @@ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], @@ -200,7 +202,7 @@ "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], - "@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="], + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], @@ -212,9 +214,9 @@ "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], - "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ=="], + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], - "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA=="], + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], @@ -222,13 +224,13 @@ "@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="], - "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.7", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ=="], + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - "@radix-ui/react-select": ["@radix-ui/react-select@2.2.5", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA=="], + "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="], "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="], @@ -254,7 +256,7 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.30", "", {}, "sha512-whXaSoNUFiyDAjkUF8OBpOm77Szdbk5lGNqFe6CbVbJFrhCCPinCbRA3NjawwlNHla1No7xvXXh+CpSxnPfUEw=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.32", "", {}, "sha512-QReCdvxiUZAPkvp1xpAg62IeNzykOFA6syH2CnClif4YmALN1XKpB39XneL80008UbtMShthSVDKmrx05N1q/g=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.46.2", "", { "os": "android", "cpu": "arm" }, "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA=="], @@ -298,41 +300,41 @@ "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], - "@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="], + "@tailwindcss/node": ["@tailwindcss/node@4.1.12", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.5.1", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.12" } }, "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ=="], - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.11", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.11", "@tailwindcss/oxide-darwin-arm64": "4.1.11", "@tailwindcss/oxide-darwin-x64": "4.1.11", "@tailwindcss/oxide-freebsd-x64": "4.1.11", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", "@tailwindcss/oxide-linux-x64-musl": "4.1.11", "@tailwindcss/oxide-wasm32-wasi": "4.1.11", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" } }, "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg=="], + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.12", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.12", "@tailwindcss/oxide-darwin-arm64": "4.1.12", "@tailwindcss/oxide-darwin-x64": "4.1.12", "@tailwindcss/oxide-freebsd-x64": "4.1.12", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.12", "@tailwindcss/oxide-linux-arm64-musl": "4.1.12", "@tailwindcss/oxide-linux-x64-gnu": "4.1.12", "@tailwindcss/oxide-linux-x64-musl": "4.1.12", "@tailwindcss/oxide-wasm32-wasi": "4.1.12", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.12", "@tailwindcss/oxide-win32-x64-msvc": "4.1.12" } }, "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw=="], - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.11", "", { "os": "android", "cpu": "arm64" }, "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg=="], + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.12", "", { "os": "android", "cpu": "arm64" }, "sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ=="], - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ=="], + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw=="], - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw=="], + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg=="], - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA=="], + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww=="], - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11", "", { "os": "linux", "cpu": "arm" }, "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg=="], + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.12", "", { "os": "linux", "cpu": "arm" }, "sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ=="], - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ=="], + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g=="], - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ=="], + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA=="], - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg=="], + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.12", "", { "os": "linux", "cpu": "x64" }, "sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q=="], - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q=="], + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.12", "", { "os": "linux", "cpu": "x64" }, "sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A=="], - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.11", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g=="], + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.12", "", { "dependencies": { "@emnapi/core": "^1.4.5", "@emnapi/runtime": "^1.4.5", "@emnapi/wasi-threads": "^1.0.4", "@napi-rs/wasm-runtime": "^0.2.12", "@tybys/wasm-util": "^0.10.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg=="], - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w=="], + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg=="], - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.11", "", { "os": "win32", "cpu": "x64" }, "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg=="], + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.12", "", { "os": "win32", "cpu": "x64" }, "sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA=="], - "@tailwindcss/vite": ["@tailwindcss/vite@4.1.11", "", { "dependencies": { "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "tailwindcss": "4.1.11" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw=="], + "@tailwindcss/vite": ["@tailwindcss/vite@4.1.12", "", { "dependencies": { "@tailwindcss/node": "4.1.12", "@tailwindcss/oxide": "4.1.12", "tailwindcss": "4.1.12" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-4pt0AMFDx7gzIrAOIYgYP0KCBuKWqyW8ayrdiLEjoJTT4pKTjrzG/e4uzWtTLDziC+66R9wbUqZBccJalSE5vQ=="], "@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.83.1", "", { "dependencies": { "@typescript-eslint/utils": "^8.37.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-tdkpPFfzkTksN9BIlT/qjixSAtKrsW6PUVRwdKWaOcag7DrD1vpki3UzzdfMQGDRGeg1Ue1Dg+rcl5FJGembNg=="], - "@tanstack/query-core": ["@tanstack/query-core@5.83.1", "", {}, "sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q=="], + "@tanstack/query-core": ["@tanstack/query-core@5.85.5", "", {}, "sha512-KO0WTob4JEApv69iYp1eGvfMSUkgw//IpMnq+//cORBzXf0smyRwPLrUvEe5qtAEGjwZTXrjxg+oJNP/C00t6w=="], - "@tanstack/react-query": ["@tanstack/react-query@5.84.1", "", { "dependencies": { "@tanstack/query-core": "5.83.1" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-zo7EUygcWJMQfFNWDSG7CBhy8irje/XY0RDVKKV4IQJAysb+ZJkkJPcnQi+KboyGUgT+SQebRFoTqLuTtfoDLw=="], + "@tanstack/react-query": ["@tanstack/react-query@5.85.5", "", { "dependencies": { "@tanstack/query-core": "5.85.5" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-/X4EFNcnPiSs8wM2v+b6DqS5mmGeuJQvxBglmDxl6ZQb5V26ouD2SJYAcC3VjbNwqhY2zjxVD15rDA5nGbMn3A=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -344,7 +346,7 @@ "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], - "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], @@ -356,39 +358,39 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@24.2.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw=="], + "@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], - "@types/react": ["@types/react@19.1.9", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA=="], + "@types/react": ["@types/react@19.1.11", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ=="], - "@types/react-dom": ["@types/react-dom@19.1.7", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw=="], + "@types/react-dom": ["@types/react-dom@19.1.8", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-xG7xaBMJCpcK0RpN8jDbAACQo54ycO6h4dSSmgv8+fu6ZIAdANkx/WsawASUjVXYfy+J9AbUpRMNNEsXCDfDBQ=="], "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.39.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/type-utils": "8.39.0", "@typescript-eslint/utils": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.39.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.41.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/type-utils": "8.41.0", "@typescript-eslint/utils": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.41.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-8fz6oa6wEKZrhXWro/S3n2eRJqlRcIa6SlDh59FXJ5Wp5XRZ8B9ixpJDcjadHq47hMx0u+HW6SNa6LjJQ6NLtw=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.39.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.41.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/types": "8.41.0", "@typescript-eslint/typescript-estree": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.39.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.39.0", "@typescript-eslint/types": "^8.39.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.41.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.41.0", "@typescript-eslint/types": "^8.41.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ=="], "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0" } }, "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.39.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.41.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0", "@typescript-eslint/utils": "8.39.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.41.0", "", { "dependencies": { "@typescript-eslint/types": "8.41.0", "@typescript-eslint/typescript-estree": "8.41.0", "@typescript-eslint/utils": "8.41.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-63qt1h91vg3KsjVVonFJWjgSK7pZHSQFKH6uwqxAH9bBrsyRhO6ONoKyXxyVBzG1lJnFAJcKAcxLS54N1ee1OQ=="], "@typescript-eslint/types": ["@typescript-eslint/types@8.38.0", "", {}, "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.39.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.39.0", "@typescript-eslint/tsconfig-utils": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.41.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.41.0", "@typescript-eslint/tsconfig-utils": "8.41.0", "@typescript-eslint/types": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ=="], "@typescript-eslint/utils": ["@typescript-eslint/utils@8.38.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.41.0", "", { "dependencies": { "@typescript-eslint/types": "8.41.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - "@vitejs/plugin-react": ["@vitejs/plugin-react@5.0.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.30", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-Jx9JfsTa05bYkS9xo0hkofp2dCmp1blrKjw9JONs5BTHOvJCgLbaPSuZLGSVJW6u2qe0tc4eevY0+gSNNi0YCw=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.0.1", "", { "dependencies": { "@babel/core": "^7.28.3", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.32", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-DE4UNaBXwtVoDJ0ccBdLVjFTWL70NRuWNCxEieTI3lrq9ORB9aOCQEKstwDXBl87NvFdbqh/p7eINGyj0BthJA=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], @@ -480,7 +482,7 @@ "electron-to-chromium": ["electron-to-chromium@1.5.151", "", {}, "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA=="], - "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], + "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], @@ -496,7 +498,7 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@9.32.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.32.0", "@eslint/plugin-kit": "^0.3.4", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg=="], + "eslint": ["eslint@9.34.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.34.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg=="], "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], @@ -530,7 +532,7 @@ "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], - "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], @@ -584,7 +586,7 @@ "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], - "i18next": ["i18next@25.3.2", "", { "dependencies": { "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-JSnbZDxRVbphc5jiptxr3o2zocy5dEqpVm9qCGdJwRNO+9saUJS0/u4LnM/13C23fUEWxAylPqKU/NpMV/IjqA=="], + "i18next": ["i18next@25.4.2", "", { "dependencies": { "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-gD4T25a6ovNXsfXY1TwHXXXLnD/K2t99jyYMCSimSCBnBRJVQr5j+VAaU83RJCPzrTGhVQ6dqIga66xO2rtd5g=="], "i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.0", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g=="], @@ -668,7 +670,7 @@ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "lucide-react": ["lucide-react@0.539.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VVISr+VF2krO91FeuCrm1rSOLACQUYVy7NQkzrOty52Y8TlTPcXcMdQFj9bYzBgXbWCiywlwSZ3Z8u6a+6bMlg=="], + "lucide-react": ["lucide-react@0.541.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-s0Vircsu5WaGv2KoJZ5+SoxiAJ3UXV5KqEM3eIFDHaHkcLIFdIWgXtZ412+Gh02UsdS7Was+jvEpBvPCWQISlg=="], "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], @@ -796,7 +798,7 @@ "react-hook-form": ["react-hook-form@7.62.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA=="], - "react-i18next": ["react-i18next@15.6.1", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-uGrzSsOUUe2sDBG/+FJq2J1MM+Y4368/QW8OLEKSFvnDflHBbZhSd1u3UkW0Z06rMhZmnB/AQrhCpYfE5/5XNg=="], + "react-i18next": ["react-i18next@15.7.2", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 25.4.1", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-xJxq7ibnhUlMvd82lNC4te1GxGUMoM1A05KKyqoqsBXVZtEvZg/fz/fnVzdlY/hhQ3SpP/79qCocZOtICGhd3g=="], "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], @@ -806,7 +808,7 @@ "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], - "react-router": ["react-router@7.8.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-r15M3+LHKgM4SOapNmsH3smAizWds1vJ0Z9C4mWaKnT9/wD7+d/0jYcj6LmOvonkrO4Rgdyp4KQ/29gWN2i1eg=="], + "react-router": ["react-router@7.8.2", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-7M2fR1JbIZ/jFWqelpvSZx+7vd7UlBTfdZqf6OSdF9g6+sfdqJDAWcak6ervbHph200ePlu+7G8LdoiC3ReyAQ=="], "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], @@ -850,7 +852,7 @@ "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="], - "tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="], + "tailwindcss": ["tailwindcss@4.1.12", "", {}, "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA=="], "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], @@ -868,13 +870,13 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "tw-animate-css": ["tw-animate-css@1.3.6", "", {}, "sha512-9dy0R9UsYEGmgf26L8UcHiLmSFTHa9+D7+dAt/G/sF5dCnPePZbfgDYinc7/UzAM7g/baVrmS6m9yEpU46d+LA=="], + "tw-animate-css": ["tw-animate-css@1.3.7", "", {}, "sha512-lvLb3hTIpB5oGsk8JmLoAjeCHV58nKa2zHYn8yWOoG5JJusH3bhJlF2DLAZ/5NmJ+jyH3ssiAx/2KmbhavJy/A=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], - "typescript-eslint": ["typescript-eslint@8.39.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.39.0", "@typescript-eslint/parser": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0", "@typescript-eslint/utils": "8.39.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q=="], + "typescript-eslint": ["typescript-eslint@8.41.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.41.0", "@typescript-eslint/parser": "8.41.0", "@typescript-eslint/typescript-estree": "8.41.0", "@typescript-eslint/utils": "8.41.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-n66rzs5OBXW3SFSnZHr2T685q1i4ODm2nulFJhMZBotaTavsS8TrI3d7bDlRSs9yWo7HmyWrN9qDu14Qv7Y0Dw=="], "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], @@ -902,7 +904,7 @@ "vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="], - "vite": ["vite@7.1.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-yJ+Mp7OyV+4S+afWo+QyoL9jFWD11QFH0i5i7JypnfTcA1rmgxCbiA8WwAICDEtZ1Z1hzrVhN8R8rGTqkTY8ZQ=="], + "vite": ["vite@7.1.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw=="], "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], @@ -914,7 +916,7 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "zod": ["zod@4.0.15", "", {}, "sha512-2IVHb9h4Mt6+UXkyMs0XbfICUh1eUrlJJAOupBHUhLRnKkruawyDddYRCs0Eizt900ntIMk9/4RksYl+FgSpcQ=="], + "zod": ["zod@4.1.3", "", {}, "sha512-1neef4bMce1hNTrxvHVKxWjKfGDn0oAli3Wy1Uwb7TRO1+wEwoZUZNP1NXIEESybOBiFnBOhI6a4m6tCLE8dog=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], @@ -926,10 +928,6 @@ "@babel/helper-module-imports/@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="], - "@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.27.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/parser": "^7.27.4", "@babel/template": "^7.27.2", "@babel/types": "^7.27.3", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA=="], - - "@babel/helpers/@babel/types": ["@babel/types@7.27.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q=="], - "@babel/template/@babel/parser": ["@babel/parser@7.27.2", "", { "dependencies": { "@babel/types": "^7.27.1" }, "bin": "./bin/babel-parser.js" }, "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw=="], "@babel/template/@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="], @@ -942,15 +940,21 @@ "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="], + "@jridgewell/remapping/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="], + + "@jridgewell/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="], + + "@tailwindcss/node/jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" }, "bundled": true }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="], - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" }, "bundled": true }, "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.4", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g=="], - "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" }, "bundled": true }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -966,25 +970,27 @@ "@types/babel__traverse/@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="], - "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0" } }, "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A=="], + "@types/estree-jsx/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], + + "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.41.0", "", { "dependencies": { "@typescript-eslint/types": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0" } }, "sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ=="], - "@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.39.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.41.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/types": "8.41.0", "@typescript-eslint/typescript-estree": "8.41.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.4", "", {}, "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="], - "@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0" } }, "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A=="], + "@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.41.0", "", { "dependencies": { "@typescript-eslint/types": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0" } }, "sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ=="], - "@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="], + "@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.41.0", "", {}, "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag=="], - "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="], + "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.41.0", "", {}, "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag=="], "@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g=="], - "@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="], + "@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.41.0", "", {}, "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag=="], - "@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.39.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ=="], + "@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.41.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/types": "8.41.0", "@typescript-eslint/typescript-estree": "8.41.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A=="], - "@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="], + "@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.41.0", "", {}, "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -992,10 +998,12 @@ "@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.38.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.38.0", "@typescript-eslint/tsconfig-utils": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ=="], - "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="], + "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.41.0", "", {}, "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "hast-util-to-jsx-runtime/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], + "i18next-browser-languagedetector/@babel/runtime": ["@babel/runtime@7.27.1", "", {}, "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog=="], "i18next-resources-to-backend/@babel/runtime": ["@babel/runtime@7.27.1", "", {}, "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog=="], @@ -1006,11 +1014,11 @@ "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - "rollup/@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "tinyglobby/fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], "tinyglobby/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], - "typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.39.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ=="], + "typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.41.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/types": "8.41.0", "@typescript-eslint/typescript-estree": "8.41.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A=="], "@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.27.1", "", { "dependencies": { "@babel/parser": "^7.27.1", "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w=="], @@ -1018,33 +1026,15 @@ "@babel/helper-module-imports/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], - "@babel/helper-module-transforms/@babel/traverse/@babel/generator": ["@babel/generator@7.27.5", "", { "dependencies": { "@babel/parser": "^7.27.5", "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw=="], - - "@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.27.5", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg=="], - - "@babel/helper-module-transforms/@babel/traverse/@babel/types": ["@babel/types@7.27.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q=="], - - "@babel/helper-module-transforms/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], - "@eslint/eslintrc/espree/acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], "@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.41.0", "", {}, "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.41.0", "", {}, "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag=="], - "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="], - - "@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="], - - "@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0" } }, "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A=="], + "@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.41.0", "", { "dependencies": { "@typescript-eslint/types": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0" } }, "sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], @@ -1058,11 +1048,9 @@ "@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], - "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0" } }, "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A=="], - - "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="], + "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.41.0", "", { "dependencies": { "@typescript-eslint/types": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0" } }, "sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.41.0", "", {}, "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag=="], "@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], } diff --git a/frontend/package.json b/frontend/package.json index 3b9db37b..2161e05e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,47 +12,47 @@ "dependencies": { "@hookform/resolvers": "^5.2.1", "@radix-ui/react-label": "^2.1.7", - "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", - "@tailwindcss/vite": "^4.1.11", - "@tanstack/react-query": "^5.84.1", + "@tailwindcss/vite": "^4.1.12", + "@tanstack/react-query": "^5.85.5", "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dompurify": "^3.2.6", - "i18next": "^25.3.2", + "i18next": "^25.4.2", "i18next-browser-languagedetector": "^8.2.0", "i18next-resources-to-backend": "^1.2.1", "input-otp": "^1.4.2", - "lucide-react": "^0.539.0", + "lucide-react": "^0.541.0", "next-themes": "^0.4.6", "react": "^19.1.1", "react-dom": "^19.1.1", "react-hook-form": "^7.62.0", - "react-i18next": "^15.6.1", + "react-i18next": "^15.7.2", "react-markdown": "^10.1.0", - "react-router": "^7.8.0", + "react-router": "^7.8.2", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", - "tailwindcss": "^4.1.11", - "zod": "^4.0.15" + "tailwindcss": "^4.1.12", + "zod": "^4.1.3" }, "devDependencies": { - "@eslint/js": "^9.32.0", + "@eslint/js": "^9.34.0", "@tanstack/eslint-plugin-query": "^5.83.1", - "@types/node": "^24.2.0", - "@types/react": "^19.1.9", - "@types/react-dom": "^19.1.7", - "@vitejs/plugin-react": "^5.0.0", - "eslint": "^9.32.0", + "@types/node": "^24.3.0", + "@types/react": "^19.1.11", + "@types/react-dom": "^19.1.8", + "@vitejs/plugin-react": "^5.0.1", + "eslint": "^9.34.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^16.3.0", "prettier": "3.6.2", - "tw-animate-css": "^1.3.6", + "tw-animate-css": "^1.3.7", "typescript": "~5.9.2", - "typescript-eslint": "^8.39.0", - "vite": "^7.1.1" + "typescript-eslint": "^8.41.0", + "vite": "^7.1.3" } } \ No newline at end of file From b9e35716ac4f378afe8f4c4df6d59fb3538a640e Mon Sep 17 00:00:00 2001 From: Stavros Date: Mon, 1 Sep 2025 18:22:42 +0300 Subject: [PATCH 007/127] feat: invalid domain warning (#332) * wip * refactor: update domain warning layout * i18n: add domain warning translations * refactor: rework hooks usage * feat: clear timeouts * fix: use useeffect to cleanup timeout * refactor: rework redirects and history storage * refactor: rename domain to root domain --- cmd/root.go | 1 - frontend/bun.lock | 5 - frontend/package.json | 1 - frontend/src/App.tsx | 4 +- .../domain-warning/domain-warning.tsx | 56 +++++++ frontend/src/components/layout/layout.tsx | 37 ++++- frontend/src/components/ui/button.tsx | 2 +- frontend/src/index.css | 2 +- frontend/src/lib/i18n/locales/en-US.json | 16 +- frontend/src/lib/i18n/locales/en.json | 16 +- frontend/src/pages/continue-page.tsx | 142 ++++++++++++------ frontend/src/pages/login-page.tsx | 54 +++++-- frontend/src/pages/logout-page.tsx | 23 ++- frontend/src/pages/totp-page.tsx | 22 ++- frontend/src/pages/unauthorized-page.tsx | 16 +- frontend/src/schemas/app-context-schema.ts | 4 +- internal/bootstrap/app_bootstrap.go | 20 +-- internal/config/config.go | 1 - internal/controller/context_controller.go | 20 ++- internal/controller/oauth_controller.go | 14 +- internal/controller/user_controller.go | 8 +- internal/middleware/context_middleware.go | 6 +- internal/service/auth_service.go | 6 +- internal/utils/app_utils.go | 6 +- 24 files changed, 332 insertions(+), 150 deletions(-) create mode 100644 frontend/src/components/domain-warning/domain-warning.tsx diff --git a/cmd/root.go b/cmd/root.go index 3ae72925..171e0433 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -95,7 +95,6 @@ func init() { {"generic-user-url", "", "Generic OAuth user info URL."}, {"generic-name", "Generic", "Generic OAuth provider name."}, {"generic-skip-ssl", false, "Skip SSL verification for the generic OAuth provider."}, - {"disable-continue", false, "Disable continue screen and redirect to app directly."}, {"oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth."}, {"oauth-auto-redirect", "none", "Auto redirect to the specified OAuth provider if configured. (available providers: github, google, generic)"}, {"session-expiry", 86400, "Session (cookie) expiration time in seconds."}, diff --git a/frontend/bun.lock b/frontend/bun.lock index 12b197ba..1f98f9c4 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -14,7 +14,6 @@ "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "dompurify": "^3.2.6", "i18next": "^25.4.2", "i18next-browser-languagedetector": "^8.2.0", "i18next-resources-to-backend": "^1.2.1", @@ -364,8 +363,6 @@ "@types/react-dom": ["@types/react-dom@19.1.8", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-xG7xaBMJCpcK0RpN8jDbAACQo54ycO6h4dSSmgv8+fu6ZIAdANkx/WsawASUjVXYfy+J9AbUpRMNNEsXCDfDBQ=="], - "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], - "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.41.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/type-utils": "8.41.0", "@typescript-eslint/utils": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.41.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-8fz6oa6wEKZrhXWro/S3n2eRJqlRcIa6SlDh59FXJ5Wp5XRZ8B9ixpJDcjadHq47hMx0u+HW6SNa6LjJQ6NLtw=="], @@ -476,8 +473,6 @@ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], - "dompurify": ["dompurify@3.2.6", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ=="], - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "electron-to-chromium": ["electron-to-chromium@1.5.151", "", {}, "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA=="], diff --git a/frontend/package.json b/frontend/package.json index 2161e05e..3d3fc47d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,7 +20,6 @@ "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "dompurify": "^3.2.6", "i18next": "^25.4.2", "i18next-browser-languagedetector": "^8.2.0", "i18next-resources-to-backend": "^1.2.1", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 72b9238b..0559b26f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,8 +5,8 @@ export const App = () => { const { isLoggedIn } = useUserContext(); if (isLoggedIn) { - return ; + return ; } - return ; + return ; }; diff --git a/frontend/src/components/domain-warning/domain-warning.tsx b/frontend/src/components/domain-warning/domain-warning.tsx new file mode 100644 index 00000000..4f83b231 --- /dev/null +++ b/frontend/src/components/domain-warning/domain-warning.tsx @@ -0,0 +1,56 @@ +import { + Card, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "../ui/card"; +import { Button } from "../ui/button"; +import { Trans, useTranslation } from "react-i18next"; +import { useLocation } from "react-router"; + +interface Props { + onClick: () => void; + appUrl: string; + currentUrl: string; +} + +export const DomainWarning = (props: Props) => { + const { onClick, appUrl, currentUrl } = props; + const { t } = useTranslation(); + const { search } = useLocation(); + + const searchParams = new URLSearchParams(search); + const redirectUri = searchParams.get("redirect_uri"); + + return ( + + + {t("domainWarningTitle")} + + }} + /> + + + + + + + + ); +}; diff --git a/frontend/src/components/layout/layout.tsx b/frontend/src/components/layout/layout.tsx index 773185b0..3461000b 100644 --- a/frontend/src/components/layout/layout.tsx +++ b/frontend/src/components/layout/layout.tsx @@ -1,8 +1,10 @@ import { useAppContext } from "@/context/app-context"; import { LanguageSelector } from "../language/language"; import { Outlet } from "react-router"; +import { useCallback, useState } from "react"; +import { DomainWarning } from "../domain-warning/domain-warning"; -export const Layout = () => { +const BaseLayout = ({ children }: { children: React.ReactNode }) => { const { backgroundImage } = useAppContext(); return ( @@ -15,7 +17,38 @@ export const Layout = () => { }} > - + {children} ); }; + +export const Layout = () => { + const { appUrl } = useAppContext(); + const [ignoreDomainWarning, setIgnoreDomainWarning] = useState(() => { + return window.sessionStorage.getItem("ignoreDomainWarning") === "true"; + }); + const currentUrl = window.location.origin; + + const handleIgnore = useCallback(() => { + window.sessionStorage.setItem("ignoreDomainWarning", "true"); + setIgnoreDomainWarning(true); + }, [setIgnoreDomainWarning]); + + if (!ignoreDomainWarning && appUrl !== currentUrl) { + return ( + + handleIgnore()} + /> + + ); + } + + return ( + + + + ); +}; diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index fbb5b27a..4badcc10 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -22,7 +22,7 @@ const buttonVariants = cva( "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", warning: - "bg-amber-500 text-white shadow-xs hover:bg-amber-400 focus-visible:ring-amber-200/20 dark:focus-visible:ring-amber-400/40 dark:bg-amber-600", + "bg-amber-500 text-white shadow-xs hover:bg-amber-400 focus-visible:ring-amber-200/20 dark:focus-visible:ring-amber-400/40", }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", diff --git a/frontend/src/index.css b/frontend/src/index.css index 0b1ee02c..97016361 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -156,7 +156,7 @@ ul { } code { - @apply relative rounded bg-muted px-[0.2rem] py-[0.1rem] font-mono text-sm font-semibold; + @apply relative rounded bg-muted px-[0.2rem] py-[0.1rem] font-mono text-sm font-semibold break-all; } .lead { diff --git a/frontend/src/lib/i18n/locales/en-US.json b/frontend/src/lib/i18n/locales/en-US.json index 74e422f5..b2dd9001 100644 --- a/frontend/src/lib/i18n/locales/en-US.json +++ b/frontend/src/lib/i18n/locales/en-US.json @@ -14,14 +14,14 @@ "loginOauthFailSubtitle": "Failed to get OAuth URL", "loginOauthSuccessTitle": "Redirecting", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", + "continueTitle": "Continue", "continueRedirectingTitle": "Redirecting...", "continueRedirectingSubtitle": "You should be redirected to the app soon", - "continueInvalidRedirectTitle": "Invalid redirect", - "continueInvalidRedirectSubtitle": "The redirect URL is invalid", + "continueRedirectManually": "Redirect me manually", "continueInsecureRedirectTitle": "Insecure redirect", "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", - "continueTitle": "Continue", - "continueSubtitle": "Click the button to continue to your app.", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{rootDomain}}). Are you sure you want to continue?", "logoutFailTitle": "Failed to log out", "logoutFailSubtitle": "Please try again", "logoutSuccessTitle": "Logged out", @@ -44,8 +44,6 @@ "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", "unauthorizedButton": "Try again", - "untrustedRedirectTitle": "Untrusted redirect", - "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", "cancelTitle": "Cancel", "forgotPasswordTitle": "Forgot your password?", "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", @@ -53,5 +51,9 @@ "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.", "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "invalidInput": "Invalid input", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index 74e422f5..b2dd9001 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -14,14 +14,14 @@ "loginOauthFailSubtitle": "Failed to get OAuth URL", "loginOauthSuccessTitle": "Redirecting", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", + "continueTitle": "Continue", "continueRedirectingTitle": "Redirecting...", "continueRedirectingSubtitle": "You should be redirected to the app soon", - "continueInvalidRedirectTitle": "Invalid redirect", - "continueInvalidRedirectSubtitle": "The redirect URL is invalid", + "continueRedirectManually": "Redirect me manually", "continueInsecureRedirectTitle": "Insecure redirect", "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", - "continueTitle": "Continue", - "continueSubtitle": "Click the button to continue to your app.", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{rootDomain}}). Are you sure you want to continue?", "logoutFailTitle": "Failed to log out", "logoutFailSubtitle": "Please try again", "logoutSuccessTitle": "Logged out", @@ -44,8 +44,6 @@ "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", "unauthorizedButton": "Try again", - "untrustedRedirectTitle": "Untrusted redirect", - "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", "cancelTitle": "Cancel", "forgotPasswordTitle": "Forgot your password?", "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", @@ -53,5 +51,9 @@ "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.", "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "invalidInput": "Invalid input", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain" } \ No newline at end of file diff --git a/frontend/src/pages/continue-page.tsx b/frontend/src/pages/continue-page.tsx index cc4d4326..261be8b8 100644 --- a/frontend/src/pages/continue-page.tsx +++ b/frontend/src/pages/continue-page.tsx @@ -11,60 +11,101 @@ import { useUserContext } from "@/context/user-context"; import { isValidUrl } from "@/lib/utils"; import { Trans, useTranslation } from "react-i18next"; import { Navigate, useLocation, useNavigate } from "react-router"; -import DOMPurify from "dompurify"; -import { useState } from "react"; +import { useEffect, useState } from "react"; export const ContinuePage = () => { + const { rootDomain } = useAppContext(); const { isLoggedIn } = useUserContext(); - - if (!isLoggedIn) { - return ; - } - - const { domain, disableContinue } = useAppContext(); const { search } = useLocation(); + const { t } = useTranslation(); + const navigate = useNavigate(); + const [loading, setLoading] = useState(false); + const [showRedirectButton, setShowRedirectButton] = useState(false); const searchParams = new URLSearchParams(search); - const redirectURI = searchParams.get("redirect_uri"); - - if (!redirectURI) { - return ; - } + const redirectUri = searchParams.get("redirect_uri"); - if (!isValidUrl(DOMPurify.sanitize(redirectURI))) { - return ; - } + const isValidRedirectUri = + redirectUri !== null ? isValidUrl(redirectUri) : false; + const redirectUriObj = isValidRedirectUri + ? new URL(redirectUri as string) + : null; + const isTrustedRedirectUri = + redirectUriObj !== null + ? redirectUriObj.hostname === rootDomain || + redirectUriObj.hostname.endsWith(`.${rootDomain}`) + : false; + const isAllowedRedirectProto = + redirectUriObj !== null + ? redirectUriObj.protocol === "https:" || + redirectUriObj.protocol === "http:" + : false; + const isHttpsDowngrade = + redirectUriObj !== null + ? redirectUriObj.protocol === "http:" && + window.location.protocol === "https:" + : false; const handleRedirect = () => { setLoading(true); - window.location.href = DOMPurify.sanitize(redirectURI); - } + window.location.assign(redirectUriObj!.toString()); + }; - if (disableContinue) { - handleRedirect(); - } + useEffect(() => { + if ( + !isLoggedIn || + !isValidRedirectUri || + !isTrustedRedirectUri || + !isAllowedRedirectProto || + isHttpsDowngrade + ) { + return; + } - const { t } = useTranslation(); - const navigate = useNavigate(); + const auto = setTimeout(() => { + handleRedirect(); + }, 100); + + const reveal = setTimeout(() => { + setLoading(false); + setShowRedirectButton(true); + }, 1000); + + return () => { + clearTimeout(auto); + clearTimeout(reveal); + }; + }, []); - const url = new URL(redirectURI); + if (!isLoggedIn) { + return ( + + ); + } - if (!(url.hostname == domain) && !url.hostname.endsWith(`.${domain}`)) { + if (!isValidRedirectUri || !isAllowedRedirectProto) { + return ; + } + + if (!isTrustedRedirectUri) { return ( - + - {t("untrustedRedirectTitle")} + {t("continueUntrustedRedirectTitle")} , }} - values={{ domain }} + values={{ rootDomain }} /> @@ -76,7 +117,11 @@ export const ContinuePage = () => { > {t("continueTitle")} - @@ -84,9 +129,9 @@ export const ContinuePage = () => { ); } - if (url.protocol === "http:" && window.location.protocol === "https:") { + if (isHttpsDowngrade) { return ( - + {t("continueInsecureRedirectTitle")} @@ -102,14 +147,14 @@ export const ContinuePage = () => { - - @@ -120,17 +165,18 @@ export const ContinuePage = () => { return ( - {t("continueTitle")} - {t("continueSubtitle")} + + {t("continueRedirectingTitle")} + + {t("continueRedirectingSubtitle")} - - - + {showRedirectButton && ( + + + + )} ); }; diff --git a/frontend/src/pages/login-page.tsx b/frontend/src/pages/login-page.tsx index 53f183f1..fd7108cb 100644 --- a/frontend/src/pages/login-page.tsx +++ b/frontend/src/pages/login-page.tsx @@ -17,23 +17,21 @@ import { useIsMounted } from "@/lib/hooks/use-is-mounted"; import { LoginSchema } from "@/schemas/login-schema"; import { useMutation } from "@tanstack/react-query"; import axios, { AxiosError } from "axios"; -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { Navigate, useLocation } from "react-router"; import { toast } from "sonner"; export const LoginPage = () => { const { isLoggedIn } = useUserContext(); - - if (isLoggedIn) { - return ; - } - - const { configuredProviders, title, oauthAutoRedirect, genericName } = useAppContext(); + const { configuredProviders, title, oauthAutoRedirect, genericName } = + useAppContext(); const { search } = useLocation(); const { t } = useTranslation(); const isMounted = useIsMounted(); + const redirectTimer = useRef(null); + const searchParams = new URLSearchParams(search); const redirectUri = searchParams.get("redirect_uri"); @@ -53,8 +51,8 @@ export const LoginPage = () => { description: t("loginOauthSuccessSubtitle"), }); - setTimeout(() => { - window.location.href = data.data.url; + redirectTimer.current = window.setTimeout(() => { + window.location.replace(data.data.url); }, 500); }, onError: () => { @@ -79,7 +77,7 @@ export const LoginPage = () => { description: t("loginSuccessSubtitle"), }); - setTimeout(() => { + redirectTimer.current = window.setTimeout(() => { window.location.replace( `/continue?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`, ); @@ -100,6 +98,7 @@ export const LoginPage = () => { if ( oauthConfigured && configuredProviders.includes(oauthAutoRedirect) && + !isLoggedIn && redirectUri ) { oauthMutation.mutate(oauthAutoRedirect); @@ -107,6 +106,26 @@ export const LoginPage = () => { } }, []); + useEffect( + () => () => { + if (redirectTimer.current) clearTimeout(redirectTimer.current); + }, + [], + ); + + if (isLoggedIn && redirectUri) { + return ( + + ); + } + + if (isLoggedIn) { + return ; + } + return ( @@ -126,7 +145,10 @@ export const LoginPage = () => { icon={} className="w-full" onClick={() => oauthMutation.mutate("google")} - loading={oauthMutation.isPending && oauthMutation.variables === "google"} + loading={ + oauthMutation.isPending && + oauthMutation.variables === "google" + } disabled={oauthMutation.isPending || loginMutation.isPending} /> )} @@ -136,7 +158,10 @@ export const LoginPage = () => { icon={} className="w-full" onClick={() => oauthMutation.mutate("github")} - loading={oauthMutation.isPending && oauthMutation.variables === "github"} + loading={ + oauthMutation.isPending && + oauthMutation.variables === "github" + } disabled={oauthMutation.isPending || loginMutation.isPending} /> )} @@ -146,7 +171,10 @@ export const LoginPage = () => { icon={} className="w-full" onClick={() => oauthMutation.mutate("generic")} - loading={oauthMutation.isPending && oauthMutation.variables === "generic"} + loading={ + oauthMutation.isPending && + oauthMutation.variables === "generic" + } disabled={oauthMutation.isPending || loginMutation.isPending} /> )} diff --git a/frontend/src/pages/logout-page.tsx b/frontend/src/pages/logout-page.tsx index 30b2af8c..17693bb6 100644 --- a/frontend/src/pages/logout-page.tsx +++ b/frontend/src/pages/logout-page.tsx @@ -11,20 +11,18 @@ import { useUserContext } from "@/context/user-context"; import { capitalize } from "@/lib/utils"; import { useMutation } from "@tanstack/react-query"; import axios from "axios"; +import { useEffect, useRef } from "react"; import { Trans, useTranslation } from "react-i18next"; import { Navigate } from "react-router"; import { toast } from "sonner"; export const LogoutPage = () => { const { provider, username, isLoggedIn, email } = useUserContext(); - - if (!isLoggedIn) { - return ; - } - const { genericName } = useAppContext(); const { t } = useTranslation(); + const redirectTimer = useRef(null); + const logoutMutation = useMutation({ mutationFn: () => axios.post("/api/user/logout"), mutationKey: ["logout"], @@ -33,8 +31,8 @@ export const LogoutPage = () => { description: t("logoutSuccessSubtitle"), }); - setTimeout(async () => { - window.location.replace("/login"); + redirectTimer.current = window.setTimeout(() => { + window.location.assign("/login"); }, 500); }, onError: () => { @@ -44,6 +42,17 @@ export const LogoutPage = () => { }, }); + useEffect( + () => () => { + if (redirectTimer.current) clearTimeout(redirectTimer.current); + }, + [], + ); + + if (!isLoggedIn) { + return ; + } + return ( diff --git a/frontend/src/pages/totp-page.tsx b/frontend/src/pages/totp-page.tsx index 7d4ebad1..ef055652 100644 --- a/frontend/src/pages/totp-page.tsx +++ b/frontend/src/pages/totp-page.tsx @@ -12,22 +12,19 @@ import { useUserContext } from "@/context/user-context"; import { TotpSchema } from "@/schemas/totp-schema"; import { useMutation } from "@tanstack/react-query"; import axios from "axios"; -import { useId } from "react"; +import { useEffect, useId, useRef } from "react"; import { useTranslation } from "react-i18next"; import { Navigate, useLocation } from "react-router"; import { toast } from "sonner"; export const TotpPage = () => { const { totpPending } = useUserContext(); - - if (!totpPending) { - return ; - } - const { t } = useTranslation(); const { search } = useLocation(); const formId = useId(); + const redirectTimer = useRef(null); + const searchParams = new URLSearchParams(search); const redirectUri = searchParams.get("redirect_uri"); @@ -39,7 +36,7 @@ export const TotpPage = () => { description: t("totpSuccessSubtitle"), }); - setTimeout(() => { + redirectTimer.current = window.setTimeout(() => { window.location.replace( `/continue?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`, ); @@ -52,6 +49,17 @@ export const TotpPage = () => { }, }); + useEffect( + () => () => { + if (redirectTimer.current) clearTimeout(redirectTimer.current); + }, + [], + ); + + if (!totpPending) { + return ; + } + return ( diff --git a/frontend/src/pages/unauthorized-page.tsx b/frontend/src/pages/unauthorized-page.tsx index e0bd6cac..007e01c5 100644 --- a/frontend/src/pages/unauthorized-page.tsx +++ b/frontend/src/pages/unauthorized-page.tsx @@ -12,6 +12,10 @@ import { Navigate, useLocation, useNavigate } from "react-router"; export const UnauthorizedPage = () => { const { search } = useLocation(); + const { t } = useTranslation(); + const navigate = useNavigate(); + + const [loading, setLoading] = useState(false); const searchParams = new URLSearchParams(search); const username = searchParams.get("username"); @@ -19,19 +23,15 @@ export const UnauthorizedPage = () => { const groupErr = searchParams.get("groupErr"); const ip = searchParams.get("ip"); - if (!username && !ip) { - return ; - } - - const { t } = useTranslation(); - const navigate = useNavigate(); - const [loading, setLoading] = useState(false); - const handleRedirect = () => { setLoading(true); navigate("/login"); }; + if (!username && !ip) { + return ; + } + let i18nKey = "unauthorizedLoginSubtitle"; if (resource) { diff --git a/frontend/src/schemas/app-context-schema.ts b/frontend/src/schemas/app-context-schema.ts index 31ded496..c5d6d85f 100644 --- a/frontend/src/schemas/app-context-schema.ts +++ b/frontend/src/schemas/app-context-schema.ts @@ -2,10 +2,10 @@ import { z } from "zod"; export const appContextSchema = z.object({ configuredProviders: z.array(z.string()), - disableContinue: z.boolean(), title: z.string(), genericName: z.string(), - domain: z.string(), + appUrl: z.string(), + rootDomain: z.string(), forgotPasswordMessage: z.string(), oauthAutoRedirect: z.enum(["none", "github", "google", "generic"]), backgroundImage: z.string(), diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index af75aa6d..d2ac1b0a 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -2,6 +2,7 @@ package bootstrap import ( "fmt" + "net/url" "strings" "tinyauth/internal/config" "tinyauth/internal/controller" @@ -44,15 +45,16 @@ func (app *BootstrapApp) Setup() error { return err } - // Get domain - domain, err := utils.GetUpperDomain(app.Config.AppURL) + // Get root domain + rootDomain, err := utils.GetRootDomain(app.Config.AppURL) if err != nil { return err } // Cookie names - cookieId := utils.GenerateIdentifier(strings.Split(domain, ".")[0]) + appUrl, _ := url.Parse(app.Config.AppURL) // Already validated + cookieId := utils.GenerateIdentifier(appUrl.Hostname()) sessionCookieName := fmt.Sprintf("%s-%s", config.SessionCookieName, cookieId) csrfCookieName := fmt.Sprintf("%s-%s", config.CSRFCookieName, cookieId) redirectCookieName := fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId) @@ -63,7 +65,7 @@ func (app *BootstrapApp) Setup() error { OauthWhitelist: app.Config.OAuthWhitelist, SessionExpiry: app.Config.SessionExpiry, SecureCookie: app.Config.SecureCookie, - Domain: domain, + RootDomain: rootDomain, LoginTimeout: app.Config.LoginTimeout, LoginMaxRetries: app.Config.LoginMaxRetries, SessionCookieName: sessionCookieName, @@ -153,7 +155,7 @@ func (app *BootstrapApp) Setup() error { var middlewares []Middleware contextMiddleware := middleware.NewContextMiddleware(middleware.ContextMiddlewareConfig{ - Domain: domain, + RootDomain: rootDomain, }, authService, oauthBrokerService) uiMiddleware := middleware.NewUIMiddleware() @@ -177,10 +179,10 @@ func (app *BootstrapApp) Setup() error { // Create controllers contextController := controller.NewContextController(controller.ContextControllerConfig{ ConfiguredProviders: configuredProviders, - DisableContinue: app.Config.DisableContinue, Title: app.Config.Title, GenericName: app.Config.GenericName, - Domain: domain, + AppURL: app.Config.AppURL, + RootDomain: rootDomain, ForgotPasswordMessage: app.Config.ForgotPasswordMessage, BackgroundImage: app.Config.BackgroundImage, OAuthAutoRedirect: app.Config.OAuthAutoRedirect, @@ -191,7 +193,7 @@ func (app *BootstrapApp) Setup() error { SecureCookie: app.Config.SecureCookie, CSRFCookieName: csrfCookieName, RedirectCookieName: redirectCookieName, - Domain: domain, + RootDomain: rootDomain, }, apiRouter, authService, oauthBrokerService) proxyController := controller.NewProxyController(controller.ProxyControllerConfig{ @@ -199,7 +201,7 @@ func (app *BootstrapApp) Setup() error { }, apiRouter, dockerService, authService) userController := controller.NewUserController(controller.UserControllerConfig{ - Domain: domain, + RootDomain: rootDomain, }, apiRouter, authService) resourcesController := controller.NewResourcesController(controller.ResourcesControllerConfig{ diff --git a/internal/config/config.go b/internal/config/config.go index c959e265..82050def 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -36,7 +36,6 @@ type Config struct { GenericUserURL string `mapstructure:"generic-user-url"` GenericName string `mapstructure:"generic-name"` GenericSkipSSL bool `mapstructure:"generic-skip-ssl"` - DisableContinue bool `mapstructure:"disable-continue"` OAuthWhitelist string `mapstructure:"oauth-whitelist"` OAuthAutoRedirect string `mapstructure:"oauth-auto-redirect" validate:"oneof=none github google generic"` SessionExpiry int `mapstructure:"session-expiry"` diff --git a/internal/controller/context_controller.go b/internal/controller/context_controller.go index c7570f0e..7cea62fe 100644 --- a/internal/controller/context_controller.go +++ b/internal/controller/context_controller.go @@ -1,6 +1,8 @@ package controller import ( + "fmt" + "net/url" "tinyauth/internal/utils" "github.com/gin-gonic/gin" @@ -15,7 +17,7 @@ type UserContextResponse struct { Name string `json:"name"` Email string `json:"email"` Provider string `json:"provider"` - Oauth bool `json:"oauth"` + OAuth bool `json:"oauth"` TotpPending bool `json:"totpPending"` } @@ -23,10 +25,10 @@ type AppContextResponse struct { Status int `json:"status"` Message string `json:"message"` ConfiguredProviders []string `json:"configuredProviders"` - DisableContinue bool `json:"disableContinue"` Title string `json:"title"` GenericName string `json:"genericName"` - Domain string `json:"domain"` + AppURL string `json:"appUrl"` + RootDomain string `json:"rootDomain"` ForgotPasswordMessage string `json:"forgotPasswordMessage"` BackgroundImage string `json:"backgroundImage"` OAuthAutoRedirect string `json:"oauthAutoRedirect"` @@ -34,10 +36,10 @@ type AppContextResponse struct { type ContextControllerConfig struct { ConfiguredProviders []string - DisableContinue bool Title string GenericName string - Domain string + AppURL string + RootDomain string ForgotPasswordMessage string BackgroundImage string OAuthAutoRedirect string @@ -72,7 +74,7 @@ func (controller *ContextController) userContextHandler(c *gin.Context) { Name: context.Name, Email: context.Email, Provider: context.Provider, - Oauth: context.OAuth, + OAuth: context.OAuth, TotpPending: context.TotpPending, } @@ -89,14 +91,16 @@ func (controller *ContextController) userContextHandler(c *gin.Context) { } func (controller *ContextController) appContextHandler(c *gin.Context) { + appUrl, _ := url.Parse(controller.Config.AppURL) // no need to check error, validated on startup + c.JSON(200, AppContextResponse{ Status: 200, Message: "Success", ConfiguredProviders: controller.Config.ConfiguredProviders, - DisableContinue: controller.Config.DisableContinue, Title: controller.Config.Title, GenericName: controller.Config.GenericName, - Domain: controller.Config.Domain, + AppURL: fmt.Sprintf("%s://%s", appUrl.Scheme, appUrl.Host), + RootDomain: controller.Config.RootDomain, ForgotPasswordMessage: controller.Config.ForgotPasswordMessage, BackgroundImage: controller.Config.BackgroundImage, OAuthAutoRedirect: controller.Config.OAuthAutoRedirect, diff --git a/internal/controller/oauth_controller.go b/internal/controller/oauth_controller.go index 31b21f09..cfac6567 100644 --- a/internal/controller/oauth_controller.go +++ b/internal/controller/oauth_controller.go @@ -23,7 +23,7 @@ type OAuthControllerConfig struct { RedirectCookieName string SecureCookie bool AppURL string - Domain string + RootDomain string } type OAuthController struct { @@ -74,13 +74,13 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) { state := service.GenerateState() authURL := service.GetAuthURL(state) - c.SetCookie(controller.Config.CSRFCookieName, state, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.Config.Domain), controller.Config.SecureCookie, true) + c.SetCookie(controller.Config.CSRFCookieName, state, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.Config.RootDomain), controller.Config.SecureCookie, true) redirectURI := c.Query("redirect_uri") - if redirectURI != "" && utils.IsRedirectSafe(redirectURI, controller.Config.Domain) { + if redirectURI != "" && utils.IsRedirectSafe(redirectURI, controller.Config.RootDomain) { log.Debug().Msg("Setting redirect URI cookie") - c.SetCookie(controller.Config.RedirectCookieName, redirectURI, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.Config.Domain), controller.Config.SecureCookie, true) + c.SetCookie(controller.Config.RedirectCookieName, redirectURI, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.Config.RootDomain), controller.Config.SecureCookie, true) } c.JSON(200, gin.H{ @@ -112,7 +112,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { return } - c.SetCookie(controller.Config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.Config.Domain), controller.Config.SecureCookie, true) + c.SetCookie(controller.Config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.Config.RootDomain), controller.Config.SecureCookie, true) code := c.Query("code") service, exists := controller.Broker.GetService(req.Provider) @@ -189,7 +189,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { redirectURI, err := c.Cookie(controller.Config.RedirectCookieName) - if err != nil || !utils.IsRedirectSafe(redirectURI, controller.Config.Domain) { + if err != nil || !utils.IsRedirectSafe(redirectURI, controller.Config.RootDomain) { log.Debug().Msg("No redirect URI cookie found, redirecting to app root") c.Redirect(http.StatusTemporaryRedirect, controller.Config.AppURL) return @@ -205,6 +205,6 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { return } - c.SetCookie(controller.Config.RedirectCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.Config.Domain), controller.Config.SecureCookie, true) + c.SetCookie(controller.Config.RedirectCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.Config.RootDomain), controller.Config.SecureCookie, true) c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/continue?%s", controller.Config.AppURL, queries.Encode())) } diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index f7f7c9e6..f3b7b515 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -22,7 +22,7 @@ type TotpRequest struct { } type UserControllerConfig struct { - Domain string + RootDomain string } type UserController struct { @@ -115,7 +115,7 @@ func (controller *UserController) loginHandler(c *gin.Context) { err := controller.Auth.CreateSessionCookie(c, &config.SessionCookie{ Username: user.Username, Name: utils.Capitalize(req.Username), - Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.Config.Domain), + Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.Config.RootDomain), Provider: "username", TotpPending: true, }) @@ -141,7 +141,7 @@ func (controller *UserController) loginHandler(c *gin.Context) { err = controller.Auth.CreateSessionCookie(c, &config.SessionCookie{ Username: req.Username, Name: utils.Capitalize(req.Username), - Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.Config.Domain), + Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.Config.RootDomain), Provider: "username", }) @@ -246,7 +246,7 @@ func (controller *UserController) totpHandler(c *gin.Context) { err = controller.Auth.CreateSessionCookie(c, &config.SessionCookie{ Username: user.Username, Name: utils.Capitalize(user.Username), - Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), controller.Config.Domain), + Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), controller.Config.RootDomain), Provider: "username", }) diff --git a/internal/middleware/context_middleware.go b/internal/middleware/context_middleware.go index ee8932a2..bca0400f 100644 --- a/internal/middleware/context_middleware.go +++ b/internal/middleware/context_middleware.go @@ -12,7 +12,7 @@ import ( ) type ContextMiddlewareConfig struct { - Domain string + RootDomain string } type ContextMiddleware struct { @@ -134,7 +134,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { c.Set("context", &config.UserContext{ Username: user.Username, Name: utils.Capitalize(user.Username), - Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), m.Config.Domain), + Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), m.Config.RootDomain), Provider: "basic", IsLoggedIn: true, TotpEnabled: user.TotpSecret != "", @@ -146,7 +146,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { c.Set("context", &config.UserContext{ Username: basic.Username, Name: utils.Capitalize(basic.Username), - Email: fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), m.Config.Domain), + Email: fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), m.Config.RootDomain), Provider: "basic", IsLoggedIn: true, }) diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index c7a27c1f..f028149b 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -28,7 +28,7 @@ type AuthServiceConfig struct { OauthWhitelist string SessionExpiry int SecureCookie bool - Domain string + RootDomain string LoginTimeout int LoginMaxRetries int SessionCookieName string @@ -216,7 +216,7 @@ func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.Sessio return err } - c.SetCookie(auth.Config.SessionCookieName, session.UUID, expiry, "/", fmt.Sprintf(".%s", auth.Config.Domain), auth.Config.SecureCookie, true) + c.SetCookie(auth.Config.SessionCookieName, session.UUID, expiry, "/", fmt.Sprintf(".%s", auth.Config.RootDomain), auth.Config.SecureCookie, true) return nil } @@ -234,7 +234,7 @@ func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error { return res.Error } - c.SetCookie(auth.Config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.Config.Domain), auth.Config.SecureCookie, true) + c.SetCookie(auth.Config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.Config.RootDomain), auth.Config.SecureCookie, true) return nil } diff --git a/internal/utils/app_utils.go b/internal/utils/app_utils.go index 85a87542..62b95922 100644 --- a/internal/utils/app_utils.go +++ b/internal/utils/app_utils.go @@ -12,8 +12,8 @@ import ( "github.com/rs/zerolog" ) -// Get upper domain parses a hostname and returns the upper domain (e.g. sub1.sub2.domain.com -> sub2.domain.com) -func GetUpperDomain(appUrl string) (string, error) { +// Get root domain parses a hostname and returns the upper domain (e.g. sub1.sub2.domain.com -> sub2.domain.com) +func GetRootDomain(appUrl string) (string, error) { appUrlParsed, err := url.Parse(appUrl) if err != nil { return "", err @@ -88,7 +88,7 @@ func IsRedirectSafe(redirectURL string, domain string) bool { return false } - upper, err := GetUpperDomain(redirectURL) + upper, err := GetRootDomain(redirectURL) if err != nil { return false From 5184c96e85cbad75870f3913cd4b2562b135db12 Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 2 Sep 2025 00:08:19 +0300 Subject: [PATCH 008/127] feat: add mutex to ldap service --- internal/service/ldap_service.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internal/service/ldap_service.go b/internal/service/ldap_service.go index 8576c4d7..b3a1d86f 100644 --- a/internal/service/ldap_service.go +++ b/internal/service/ldap_service.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "fmt" + "sync" "time" "github.com/cenkalti/backoff/v5" @@ -23,6 +24,7 @@ type LdapServiceConfig struct { type LdapService struct { Config LdapServiceConfig Conn *ldapgo.Conn + Mutex sync.RWMutex } func NewLdapService(config LdapServiceConfig) *LdapService { @@ -55,6 +57,8 @@ func (ldap *LdapService) Init() error { } func (ldap *LdapService) connect() (*ldapgo.Conn, error) { + ldap.Mutex.Lock() + conn, err := ldapgo.DialURL(ldap.Config.Address, ldapgo.DialWithTLSConfig(&tls.Config{ InsecureSkipVerify: ldap.Config.Insecure, MinVersion: tls.VersionTLS12, @@ -68,6 +72,8 @@ func (ldap *LdapService) connect() (*ldapgo.Conn, error) { return nil, err } + ldap.Mutex.Unlock() + // Set and return the connection ldap.Conn = conn return conn, nil @@ -86,10 +92,12 @@ func (ldap *LdapService) Search(username string) (string, error) { nil, ) + ldap.Mutex.Lock() searchResult, err := ldap.Conn.Search(searchRequest) if err != nil { return "", err } + ldap.Mutex.Unlock() if len(searchResult.Entries) != 1 { return "", fmt.Errorf("multiple or no entries found for user %s", username) @@ -100,10 +108,12 @@ func (ldap *LdapService) Search(username string) (string, error) { } func (ldap *LdapService) Bind(userDN string, password string) error { + ldap.Mutex.Lock() err := ldap.Conn.Bind(userDN, password) if err != nil { return err } + ldap.Mutex.Unlock() return nil } @@ -118,10 +128,12 @@ func (ldap *LdapService) heartbeat() error { nil, ) + ldap.Mutex.Lock() _, err := ldap.Conn.Search(searchRequest) if err != nil { return err } + ldap.Mutex.Unlock() // No error means the connection is alive return nil From ad4fc7ef5fd41ee1be05647d165ae2d069b4d1a1 Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 2 Sep 2025 01:27:55 +0300 Subject: [PATCH 009/127] refactor: don't export non-needed fields (#336) * refactor: don't export non-needed fields * feat: coderabbit suggestions * fix: avoid queries panic --- internal/controller/context_controller.go | 26 +-- internal/controller/health_controller.go | 8 +- internal/controller/oauth_controller.go | 78 ++++----- internal/controller/proxy_controller.go | 168 ++++++++------------ internal/controller/resources_controller.go | 18 +-- internal/controller/user_controller.go | 54 +++---- internal/middleware/context_middleware.go | 42 ++--- internal/middleware/ui_middleware.go | 12 +- internal/service/auth_service.go | 84 +++++----- internal/service/database_service.go | 12 +- internal/service/docker_service.go | 26 +-- internal/service/generic_oauth_service.go | 34 ++-- internal/service/github_oauth_service.go | 22 +-- internal/service/google_oauth_service.go | 22 +-- internal/service/ldap_service.go | 34 ++-- internal/service/oauth_broker_service.go | 32 ++-- 16 files changed, 325 insertions(+), 347 deletions(-) diff --git a/internal/controller/context_controller.go b/internal/controller/context_controller.go index 7cea62fe..d285da3c 100644 --- a/internal/controller/context_controller.go +++ b/internal/controller/context_controller.go @@ -46,19 +46,19 @@ type ContextControllerConfig struct { } type ContextController struct { - Config ContextControllerConfig - Router *gin.RouterGroup + config ContextControllerConfig + router *gin.RouterGroup } func NewContextController(config ContextControllerConfig, router *gin.RouterGroup) *ContextController { return &ContextController{ - Config: config, - Router: router, + config: config, + router: router, } } func (controller *ContextController) SetupRoutes() { - contextGroup := controller.Router.Group("/context") + contextGroup := controller.router.Group("/context") contextGroup.GET("/user", controller.userContextHandler) contextGroup.GET("/app", controller.appContextHandler) } @@ -91,18 +91,18 @@ func (controller *ContextController) userContextHandler(c *gin.Context) { } func (controller *ContextController) appContextHandler(c *gin.Context) { - appUrl, _ := url.Parse(controller.Config.AppURL) // no need to check error, validated on startup + appUrl, _ := url.Parse(controller.config.AppURL) // no need to check error, validated on startup c.JSON(200, AppContextResponse{ Status: 200, Message: "Success", - ConfiguredProviders: controller.Config.ConfiguredProviders, - Title: controller.Config.Title, - GenericName: controller.Config.GenericName, + ConfiguredProviders: controller.config.ConfiguredProviders, + Title: controller.config.Title, + GenericName: controller.config.GenericName, AppURL: fmt.Sprintf("%s://%s", appUrl.Scheme, appUrl.Host), - RootDomain: controller.Config.RootDomain, - ForgotPasswordMessage: controller.Config.ForgotPasswordMessage, - BackgroundImage: controller.Config.BackgroundImage, - OAuthAutoRedirect: controller.Config.OAuthAutoRedirect, + RootDomain: controller.config.RootDomain, + ForgotPasswordMessage: controller.config.ForgotPasswordMessage, + BackgroundImage: controller.config.BackgroundImage, + OAuthAutoRedirect: controller.config.OAuthAutoRedirect, }) } diff --git a/internal/controller/health_controller.go b/internal/controller/health_controller.go index 842b3d30..8f0aa424 100644 --- a/internal/controller/health_controller.go +++ b/internal/controller/health_controller.go @@ -3,18 +3,18 @@ package controller import "github.com/gin-gonic/gin" type HealthController struct { - Router *gin.RouterGroup + router *gin.RouterGroup } func NewHealthController(router *gin.RouterGroup) *HealthController { return &HealthController{ - Router: router, + router: router, } } func (controller *HealthController) SetupRoutes() { - controller.Router.GET("/health", controller.healthHandler) - controller.Router.HEAD("/health", controller.healthHandler) + controller.router.GET("/health", controller.healthHandler) + controller.router.HEAD("/health", controller.healthHandler) } func (controller *HealthController) healthHandler(c *gin.Context) { diff --git a/internal/controller/oauth_controller.go b/internal/controller/oauth_controller.go index cfac6567..23d00de9 100644 --- a/internal/controller/oauth_controller.go +++ b/internal/controller/oauth_controller.go @@ -27,23 +27,23 @@ type OAuthControllerConfig struct { } type OAuthController struct { - Config OAuthControllerConfig - Router *gin.RouterGroup - Auth *service.AuthService - Broker *service.OAuthBrokerService + config OAuthControllerConfig + router *gin.RouterGroup + auth *service.AuthService + broker *service.OAuthBrokerService } func NewOAuthController(config OAuthControllerConfig, router *gin.RouterGroup, auth *service.AuthService, broker *service.OAuthBrokerService) *OAuthController { return &OAuthController{ - Config: config, - Router: router, - Auth: auth, - Broker: broker, + config: config, + router: router, + auth: auth, + broker: broker, } } func (controller *OAuthController) SetupRoutes() { - oauthGroup := controller.Router.Group("/oauth") + oauthGroup := controller.router.Group("/oauth") oauthGroup.GET("/url/:provider", controller.oauthURLHandler) oauthGroup.GET("/callback/:provider", controller.oauthCallbackHandler) } @@ -61,7 +61,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) { return } - service, exists := controller.Broker.GetService(req.Provider) + service, exists := controller.broker.GetService(req.Provider) if !exists { log.Warn().Msgf("OAuth provider not found: %s", req.Provider) @@ -74,13 +74,13 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) { state := service.GenerateState() authURL := service.GetAuthURL(state) - c.SetCookie(controller.Config.CSRFCookieName, state, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.Config.RootDomain), controller.Config.SecureCookie, true) + c.SetCookie(controller.config.CSRFCookieName, state, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.RootDomain), controller.config.SecureCookie, true) redirectURI := c.Query("redirect_uri") - if redirectURI != "" && utils.IsRedirectSafe(redirectURI, controller.Config.RootDomain) { + if redirectURI != "" && utils.IsRedirectSafe(redirectURI, controller.config.RootDomain) { log.Debug().Msg("Setting redirect URI cookie") - c.SetCookie(controller.Config.RedirectCookieName, redirectURI, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.Config.RootDomain), controller.Config.SecureCookie, true) + c.SetCookie(controller.config.RedirectCookieName, redirectURI, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.RootDomain), controller.config.SecureCookie, true) } c.JSON(200, gin.H{ @@ -104,58 +104,58 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { } state := c.Query("state") - csrfCookie, err := c.Cookie(controller.Config.CSRFCookieName) + csrfCookie, err := c.Cookie(controller.config.CSRFCookieName) if err != nil || state != csrfCookie { log.Warn().Err(err).Msg("CSRF token mismatch or cookie missing") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) return } - c.SetCookie(controller.Config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.Config.RootDomain), controller.Config.SecureCookie, true) + c.SetCookie(controller.config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.RootDomain), controller.config.SecureCookie, true) code := c.Query("code") - service, exists := controller.Broker.GetService(req.Provider) + service, exists := controller.broker.GetService(req.Provider) if !exists { log.Warn().Msgf("OAuth provider not found: %s", req.Provider) - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) return } err = service.VerifyCode(code) if err != nil { log.Error().Err(err).Msg("Failed to verify OAuth code") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) return } - user, err := controller.Broker.GetUser(req.Provider) + user, err := controller.broker.GetUser(req.Provider) if err != nil { log.Error().Err(err).Msg("Failed to get user from OAuth provider") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) return } if user.Email == "" { log.Error().Msg("OAuth provider did not return an email") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) return } - if !controller.Auth.IsEmailWhitelisted(user.Email) { + if !controller.auth.IsEmailWhitelisted(user.Email) { queries, err := query.Values(config.UnauthorizedQuery{ Username: user.Email, }) if err != nil { log.Error().Err(err).Msg("Failed to encode unauthorized query") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) return } - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.Config.AppURL, queries.Encode())) + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.config.AppURL, queries.Encode())) return } @@ -169,29 +169,35 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { name = fmt.Sprintf("%s (%s)", utils.Capitalize(strings.Split(user.Email, "@")[0]), strings.Split(user.Email, "@")[1]) } - var usename string + var username string if user.PreferredUsername != "" { log.Debug().Msg("Using preferred username from OAuth provider") - usename = user.PreferredUsername + username = user.PreferredUsername } else { log.Debug().Msg("No preferred username from OAuth provider, using pseudo username") - usename = strings.Replace(user.Email, "@", "_", -1) + username = strings.Replace(user.Email, "@", "_", -1) } - controller.Auth.CreateSessionCookie(c, &config.SessionCookie{ - Username: usename, + err = controller.auth.CreateSessionCookie(c, &config.SessionCookie{ + Username: username, Name: name, Email: user.Email, Provider: req.Provider, OAuthGroups: utils.CoalesceToString(user.Groups), }) - redirectURI, err := c.Cookie(controller.Config.RedirectCookieName) + if err != nil { + log.Error().Err(err).Msg("Failed to create session cookie") + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) + return + } + + redirectURI, err := c.Cookie(controller.config.RedirectCookieName) - if err != nil || !utils.IsRedirectSafe(redirectURI, controller.Config.RootDomain) { + if err != nil || !utils.IsRedirectSafe(redirectURI, controller.config.RootDomain) { log.Debug().Msg("No redirect URI cookie found, redirecting to app root") - c.Redirect(http.StatusTemporaryRedirect, controller.Config.AppURL) + c.Redirect(http.StatusTemporaryRedirect, controller.config.AppURL) return } @@ -201,10 +207,10 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { if err != nil { log.Error().Err(err).Msg("Failed to encode redirect URI query") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) return } - c.SetCookie(controller.Config.RedirectCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.Config.RootDomain), controller.Config.SecureCookie, true) - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/continue?%s", controller.Config.AppURL, queries.Encode())) + c.SetCookie(controller.config.RedirectCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.RootDomain), controller.config.SecureCookie, true) + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/continue?%s", controller.config.AppURL, queries.Encode())) } diff --git a/internal/controller/proxy_controller.go b/internal/controller/proxy_controller.go index 6ad10eca..fd250768 100644 --- a/internal/controller/proxy_controller.go +++ b/internal/controller/proxy_controller.go @@ -22,23 +22,23 @@ type ProxyControllerConfig struct { } type ProxyController struct { - Config ProxyControllerConfig - Router *gin.RouterGroup - Docker *service.DockerService - Auth *service.AuthService + config ProxyControllerConfig + router *gin.RouterGroup + docker *service.DockerService + auth *service.AuthService } func NewProxyController(config ProxyControllerConfig, router *gin.RouterGroup, docker *service.DockerService, auth *service.AuthService) *ProxyController { return &ProxyController{ - Config: config, - Router: router, - Docker: docker, - Auth: auth, + config: config, + router: router, + docker: docker, + auth: auth, } } func (controller *ProxyController) SetupRoutes() { - proxyGroup := controller.Router.Group("/auth") + proxyGroup := controller.router.Group("/auth") proxyGroup.GET("/:proxy", controller.proxyHandler) } @@ -67,44 +67,18 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { proto := c.Request.Header.Get("X-Forwarded-Proto") host := c.Request.Header.Get("X-Forwarded-Host") - hostWithoutPort := strings.Split(host, ":")[0] - id := strings.Split(hostWithoutPort, ".")[0] - - labels, err := controller.Docker.GetLabels(id, hostWithoutPort) + labels, err := controller.docker.GetLabels(host) if err != nil { log.Error().Err(err).Msg("Failed to get labels from Docker") - - if req.Proxy == "nginx" || !isBrowser { - c.JSON(500, gin.H{ - "status": 500, - "message": "Internal Server Error", - }) - return - } - - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) + controller.handleError(c, req, isBrowser) return } clientIP := c.ClientIP() - if controller.Auth.IsBypassedIP(labels.IP, clientIP) { - c.Header("Authorization", c.Request.Header.Get("Authorization")) - - headers := utils.ParseHeaders(labels.Response.Headers) - - for key, value := range headers { - log.Debug().Str("header", key).Msg("Setting header") - c.Header(key, value) - } - - basicPassword := utils.GetSecret(labels.Response.BasicAuth.Password, labels.Response.BasicAuth.PasswordFile) - if labels.Response.BasicAuth.Username != "" && basicPassword != "" { - log.Debug().Str("username", labels.Response.BasicAuth.Username).Msg("Setting basic auth header") - c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(labels.Response.BasicAuth.Username, basicPassword))) - } - + if controller.auth.IsBypassedIP(labels.IP, clientIP) { + controller.setHeaders(c, labels) c.JSON(200, gin.H{ "status": 200, "message": "Authenticated", @@ -112,41 +86,17 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { return } - authEnabled, err := controller.Auth.IsAuthEnabled(uri, labels.Path) + authEnabled, err := controller.auth.IsAuthEnabled(uri, labels.Path) if err != nil { log.Error().Err(err).Msg("Failed to check if auth is enabled for resource") - - if req.Proxy == "nginx" || !isBrowser { - c.JSON(500, gin.H{ - "status": 500, - "message": "Internal Server Error", - }) - return - } - - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) + controller.handleError(c, req, isBrowser) return } if !authEnabled { log.Debug().Msg("Authentication disabled for resource, allowing access") - - c.Header("Authorization", c.Request.Header.Get("Authorization")) - - headers := utils.ParseHeaders(labels.Response.Headers) - - for key, value := range headers { - log.Debug().Str("header", key).Msg("Setting header") - c.Header(key, value) - } - - basicPassword := utils.GetSecret(labels.Response.BasicAuth.Password, labels.Response.BasicAuth.PasswordFile) - if labels.Response.BasicAuth.Username != "" && basicPassword != "" { - log.Debug().Str("username", labels.Response.BasicAuth.Username).Msg("Setting basic auth header") - c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(labels.Response.BasicAuth.Username, basicPassword))) - } - + controller.setHeaders(c, labels) c.JSON(200, gin.H{ "status": 200, "message": "Authenticated", @@ -154,7 +104,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { return } - if !controller.Auth.CheckIP(labels.IP, clientIP) { + if !controller.auth.CheckIP(labels.IP, clientIP) { if req.Proxy == "nginx" || !isBrowser { c.JSON(401, gin.H{ "status": 401, @@ -170,11 +120,11 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { if err != nil { log.Error().Err(err).Msg("Failed to encode unauthorized query") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) return } - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.Config.AppURL, queries.Encode())) + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.config.AppURL, queries.Encode())) return } @@ -197,7 +147,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { } if userContext.IsLoggedIn { - appAllowed := controller.Auth.IsResourceAllowed(c, userContext, labels) + appAllowed := controller.auth.IsResourceAllowed(c, userContext, labels) if !appAllowed { log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource") @@ -214,24 +164,24 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { Resource: strings.Split(host, ".")[0], }) + if err != nil { + log.Error().Err(err).Msg("Failed to encode unauthorized query") + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) + return + } + if userContext.OAuth { queries.Set("username", userContext.Email) } else { queries.Set("username", userContext.Username) } - if err != nil { - log.Error().Err(err).Msg("Failed to encode unauthorized query") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) - return - } - - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.Config.AppURL, queries.Encode())) + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.config.AppURL, queries.Encode())) return } if userContext.OAuth { - groupOK := controller.Auth.IsInOAuthGroup(c, userContext, labels.OAuth.Groups) + groupOK := controller.auth.IsInOAuthGroup(c, userContext, labels.OAuth.Groups) if !groupOK { log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements") @@ -249,41 +199,29 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { GroupErr: true, }) + if err != nil { + log.Error().Err(err).Msg("Failed to encode unauthorized query") + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) + return + } + if userContext.OAuth { queries.Set("username", userContext.Email) } else { queries.Set("username", userContext.Username) } - if err != nil { - log.Error().Err(err).Msg("Failed to encode unauthorized query") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) - return - } - - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.Config.AppURL, queries.Encode())) + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.config.AppURL, queries.Encode())) return } } - c.Header("Authorization", c.Request.Header.Get("Authorization")) c.Header("Remote-User", utils.SanitizeHeader(userContext.Username)) c.Header("Remote-Name", utils.SanitizeHeader(userContext.Name)) c.Header("Remote-Email", utils.SanitizeHeader(userContext.Email)) c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups)) - headers := utils.ParseHeaders(labels.Response.Headers) - - for key, value := range headers { - log.Debug().Str("header", key).Msg("Setting header") - c.Header(key, value) - } - - basicPassword := utils.GetSecret(labels.Response.BasicAuth.Password, labels.Response.BasicAuth.PasswordFile) - if labels.Response.BasicAuth.Username != "" && basicPassword != "" { - log.Debug().Str("username", labels.Response.BasicAuth.Username).Msg("Setting basic auth header") - c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(labels.Response.BasicAuth.Username, basicPassword))) - } + controller.setHeaders(c, labels) c.JSON(200, gin.H{ "status": 200, @@ -306,9 +244,39 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { if err != nil { log.Error().Err(err).Msg("Failed to encode redirect URI query") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL)) + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) + return + } + + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/login?%s", controller.config.AppURL, queries.Encode())) +} + +func (controller *ProxyController) setHeaders(c *gin.Context, labels config.AppLabels) { + c.Header("Authorization", c.Request.Header.Get("Authorization")) + + headers := utils.ParseHeaders(labels.Response.Headers) + + for key, value := range headers { + log.Debug().Str("header", key).Msg("Setting header") + c.Header(key, value) + } + + basicPassword := utils.GetSecret(labels.Response.BasicAuth.Password, labels.Response.BasicAuth.PasswordFile) + + if labels.Response.BasicAuth.Username != "" && basicPassword != "" { + log.Debug().Str("username", labels.Response.BasicAuth.Username).Msg("Setting basic auth header") + c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(labels.Response.BasicAuth.Username, basicPassword))) + } +} + +func (controller *ProxyController) handleError(c *gin.Context, req Proxy, isBrowser bool) { + if req.Proxy == "nginx" || !isBrowser { + c.JSON(500, gin.H{ + "status": 500, + "message": "Internal Server Error", + }) return } - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/login?%s", controller.Config.AppURL, queries.Encode())) + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) } diff --git a/internal/controller/resources_controller.go b/internal/controller/resources_controller.go index 56bae87d..92384e75 100644 --- a/internal/controller/resources_controller.go +++ b/internal/controller/resources_controller.go @@ -11,32 +11,32 @@ type ResourcesControllerConfig struct { } type ResourcesController struct { - Config ResourcesControllerConfig - Router *gin.RouterGroup - FileServer http.Handler + config ResourcesControllerConfig + router *gin.RouterGroup + fileServer http.Handler } func NewResourcesController(config ResourcesControllerConfig, router *gin.RouterGroup) *ResourcesController { fileServer := http.StripPrefix("/resources", http.FileServer(http.Dir(config.ResourcesDir))) return &ResourcesController{ - Config: config, - Router: router, - FileServer: fileServer, + config: config, + router: router, + fileServer: fileServer, } } func (controller *ResourcesController) SetupRoutes() { - controller.Router.GET("/resources/*resource", controller.resourcesHandler) + controller.router.GET("/resources/*resource", controller.resourcesHandler) } func (controller *ResourcesController) resourcesHandler(c *gin.Context) { - if controller.Config.ResourcesDir == "" { + if controller.config.ResourcesDir == "" { c.JSON(404, gin.H{ "status": 404, "message": "Resources not found", }) return } - controller.FileServer.ServeHTTP(c.Writer, c.Request) + controller.fileServer.ServeHTTP(c.Writer, c.Request) } diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index f3b7b515..7b486523 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -26,21 +26,21 @@ type UserControllerConfig struct { } type UserController struct { - Config UserControllerConfig - Router *gin.RouterGroup - Auth *service.AuthService + config UserControllerConfig + router *gin.RouterGroup + auth *service.AuthService } func NewUserController(config UserControllerConfig, router *gin.RouterGroup, auth *service.AuthService) *UserController { return &UserController{ - Config: config, - Router: router, - Auth: auth, + config: config, + router: router, + auth: auth, } } func (controller *UserController) SetupRoutes() { - userGroup := controller.Router.Group("/user") + userGroup := controller.router.Group("/user") userGroup.POST("/login", controller.loginHandler) userGroup.POST("/logout", controller.logoutHandler) userGroup.POST("/totp", controller.totpHandler) @@ -69,7 +69,7 @@ func (controller *UserController) loginHandler(c *gin.Context) { log.Debug().Str("username", req.Username).Str("ip", clientIP).Msg("Login attempt") - isLocked, remainingTime := controller.Auth.IsAccountLocked(rateIdentifier) + isLocked, remainingTime := controller.auth.IsAccountLocked(rateIdentifier) if isLocked { log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("Account is locked due to too many failed login attempts") @@ -80,11 +80,11 @@ func (controller *UserController) loginHandler(c *gin.Context) { return } - userSearch := controller.Auth.SearchUser(req.Username) + userSearch := controller.auth.SearchUser(req.Username) - if userSearch.Type == "" { + if userSearch.Type == "unknown" { log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("User not found") - controller.Auth.RecordLoginAttempt(rateIdentifier, false) + controller.auth.RecordLoginAttempt(rateIdentifier, false) c.JSON(401, gin.H{ "status": 401, "message": "Unauthorized", @@ -92,9 +92,9 @@ func (controller *UserController) loginHandler(c *gin.Context) { return } - if !controller.Auth.VerifyUser(userSearch, req.Password) { + if !controller.auth.VerifyUser(userSearch, req.Password) { log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("Invalid password") - controller.Auth.RecordLoginAttempt(rateIdentifier, false) + controller.auth.RecordLoginAttempt(rateIdentifier, false) c.JSON(401, gin.H{ "status": 401, "message": "Unauthorized", @@ -104,18 +104,18 @@ func (controller *UserController) loginHandler(c *gin.Context) { log.Info().Str("username", req.Username).Str("ip", clientIP).Msg("Login successful") - controller.Auth.RecordLoginAttempt(rateIdentifier, true) + controller.auth.RecordLoginAttempt(rateIdentifier, true) if userSearch.Type == "local" { - user := controller.Auth.GetLocalUser(userSearch.Username) + user := controller.auth.GetLocalUser(userSearch.Username) if user.TotpSecret != "" { log.Debug().Str("username", req.Username).Msg("User has TOTP enabled, requiring TOTP verification") - err := controller.Auth.CreateSessionCookie(c, &config.SessionCookie{ + err := controller.auth.CreateSessionCookie(c, &config.SessionCookie{ Username: user.Username, Name: utils.Capitalize(req.Username), - Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.Config.RootDomain), + Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.RootDomain), Provider: "username", TotpPending: true, }) @@ -138,10 +138,10 @@ func (controller *UserController) loginHandler(c *gin.Context) { } } - err = controller.Auth.CreateSessionCookie(c, &config.SessionCookie{ + err = controller.auth.CreateSessionCookie(c, &config.SessionCookie{ Username: req.Username, Name: utils.Capitalize(req.Username), - Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.Config.RootDomain), + Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.RootDomain), Provider: "username", }) @@ -163,7 +163,7 @@ func (controller *UserController) loginHandler(c *gin.Context) { func (controller *UserController) logoutHandler(c *gin.Context) { log.Debug().Msg("Logout request received") - controller.Auth.DeleteSessionCookie(c) + controller.auth.DeleteSessionCookie(c) c.JSON(200, gin.H{ "status": 200, @@ -214,24 +214,24 @@ func (controller *UserController) totpHandler(c *gin.Context) { log.Debug().Str("username", context.Username).Str("ip", clientIP).Msg("TOTP verification attempt") - isLocked, remainingTime := controller.Auth.IsAccountLocked(rateIdentifier) + isLocked, remainingTime := controller.auth.IsAccountLocked(rateIdentifier) if isLocked { log.Warn().Str("username", context.Username).Str("ip", clientIP).Msg("Account is locked due to too many failed TOTP attempts") c.JSON(429, gin.H{ "status": 429, - "message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remainingTime), + "message": fmt.Sprintf("Too many failed TOTP attempts. Try again in %d seconds", remainingTime), }) return } - user := controller.Auth.GetLocalUser(context.Username) + user := controller.auth.GetLocalUser(context.Username) ok := totp.Validate(req.Code, user.TotpSecret) if !ok { log.Warn().Str("username", context.Username).Str("ip", clientIP).Msg("Invalid TOTP code") - controller.Auth.RecordLoginAttempt(rateIdentifier, false) + controller.auth.RecordLoginAttempt(rateIdentifier, false) c.JSON(401, gin.H{ "status": 401, "message": "Unauthorized", @@ -241,12 +241,12 @@ func (controller *UserController) totpHandler(c *gin.Context) { log.Info().Str("username", context.Username).Str("ip", clientIP).Msg("TOTP verification successful") - controller.Auth.RecordLoginAttempt(rateIdentifier, true) + controller.auth.RecordLoginAttempt(rateIdentifier, true) - err = controller.Auth.CreateSessionCookie(c, &config.SessionCookie{ + err = controller.auth.CreateSessionCookie(c, &config.SessionCookie{ Username: user.Username, Name: utils.Capitalize(user.Username), - Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), controller.Config.RootDomain), + Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), controller.config.RootDomain), Provider: "username", }) diff --git a/internal/middleware/context_middleware.go b/internal/middleware/context_middleware.go index bca0400f..cbf94120 100644 --- a/internal/middleware/context_middleware.go +++ b/internal/middleware/context_middleware.go @@ -16,16 +16,16 @@ type ContextMiddlewareConfig struct { } type ContextMiddleware struct { - Config ContextMiddlewareConfig - Auth *service.AuthService - Broker *service.OAuthBrokerService + config ContextMiddlewareConfig + auth *service.AuthService + broker *service.OAuthBrokerService } func NewContextMiddleware(config ContextMiddlewareConfig, auth *service.AuthService, broker *service.OAuthBrokerService) *ContextMiddleware { return &ContextMiddleware{ - Config: config, - Auth: auth, - Broker: broker, + config: config, + auth: auth, + broker: broker, } } @@ -35,7 +35,7 @@ func (m *ContextMiddleware) Init() error { func (m *ContextMiddleware) Middleware() gin.HandlerFunc { return func(c *gin.Context) { - cookie, err := m.Auth.GetSessionCookie(c) + cookie, err := m.auth.GetSessionCookie(c) if err != nil { log.Debug().Err(err).Msg("No valid session cookie found") @@ -57,11 +57,11 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { switch cookie.Provider { case "username": - userSearch := m.Auth.SearchUser(cookie.Username) + userSearch := m.auth.SearchUser(cookie.Username) - if userSearch.Type == "unknown" { + if userSearch.Type == "unknown" || userSearch.Type == "error" { log.Debug().Msg("User from session cookie not found") - m.Auth.DeleteSessionCookie(c) + m.auth.DeleteSessionCookie(c) goto basic } @@ -75,17 +75,17 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { c.Next() return default: - _, exists := m.Broker.GetService(cookie.Provider) + _, exists := m.broker.GetService(cookie.Provider) if !exists { log.Debug().Msg("OAuth provider from session cookie not found") - m.Auth.DeleteSessionCookie(c) + m.auth.DeleteSessionCookie(c) goto basic } - if !m.Auth.IsEmailWhitelisted(cookie.Email) { + if !m.auth.IsEmailWhitelisted(cookie.Email) { log.Debug().Msg("Email from session cookie not whitelisted") - m.Auth.DeleteSessionCookie(c) + m.auth.DeleteSessionCookie(c) goto basic } @@ -103,7 +103,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { } basic: - basic := m.Auth.GetBasicAuth(c) + basic := m.auth.GetBasicAuth(c) if basic == nil { log.Debug().Msg("No basic auth provided") @@ -111,15 +111,15 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { return } - userSearch := m.Auth.SearchUser(basic.Username) + userSearch := m.auth.SearchUser(basic.Username) - if userSearch.Type == "unknown" { + if userSearch.Type == "unknown" || userSearch.Type == "error" { log.Debug().Msg("User from basic auth not found") c.Next() return } - if !m.Auth.VerifyUser(userSearch, basic.Password) { + if !m.auth.VerifyUser(userSearch, basic.Password) { log.Debug().Msg("Invalid password for basic auth user") c.Next() return @@ -129,12 +129,12 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { case "local": log.Debug().Msg("Basic auth user is local") - user := m.Auth.GetLocalUser(basic.Username) + user := m.auth.GetLocalUser(basic.Username) c.Set("context", &config.UserContext{ Username: user.Username, Name: utils.Capitalize(user.Username), - Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), m.Config.RootDomain), + Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), m.config.RootDomain), Provider: "basic", IsLoggedIn: true, TotpEnabled: user.TotpSecret != "", @@ -146,7 +146,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { c.Set("context", &config.UserContext{ Username: basic.Username, Name: utils.Capitalize(basic.Username), - Email: fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), m.Config.RootDomain), + Email: fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), m.config.RootDomain), Provider: "basic", IsLoggedIn: true, }) diff --git a/internal/middleware/ui_middleware.go b/internal/middleware/ui_middleware.go index dcfaa35b..ff028a16 100644 --- a/internal/middleware/ui_middleware.go +++ b/internal/middleware/ui_middleware.go @@ -11,8 +11,8 @@ import ( ) type UIMiddleware struct { - UIFS fs.FS - UIFileServer http.Handler + uiFs fs.FS + uiFileServer http.Handler } func NewUIMiddleware() *UIMiddleware { @@ -26,8 +26,8 @@ func (m *UIMiddleware) Init() error { return err } - m.UIFS = ui - m.UIFileServer = http.FileServer(http.FS(ui)) + m.uiFs = ui + m.uiFileServer = http.FileServer(http.FS(ui)) return nil } @@ -42,13 +42,13 @@ func (m *UIMiddleware) Middleware() gin.HandlerFunc { c.Next() return default: - _, err := fs.Stat(m.UIFS, strings.TrimPrefix(c.Request.URL.Path, "/")) + _, err := fs.Stat(m.uiFs, strings.TrimPrefix(c.Request.URL.Path, "/")) if os.IsNotExist(err) { c.Request.URL.Path = "/" } - m.UIFileServer.ServeHTTP(c.Writer, c.Request) + m.uiFileServer.ServeHTTP(c.Writer, c.Request) c.Abort() return } diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index f028149b..cb14a7e9 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -35,21 +35,21 @@ type AuthServiceConfig struct { } type AuthService struct { - Config AuthServiceConfig - Docker *DockerService - LoginAttempts map[string]*LoginAttempt - LoginMutex sync.RWMutex - LDAP *LdapService - Database *gorm.DB + config AuthServiceConfig + docker *DockerService + loginAttempts map[string]*LoginAttempt + loginMutex sync.RWMutex + ldap *LdapService + database *gorm.DB } func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, database *gorm.DB) *AuthService { return &AuthService{ - Config: config, - Docker: docker, - LoginAttempts: make(map[string]*LoginAttempt), - LDAP: ldap, - Database: database, + config: config, + docker: docker, + loginAttempts: make(map[string]*LoginAttempt), + ldap: ldap, + database: database, } } @@ -65,12 +65,14 @@ func (auth *AuthService) SearchUser(username string) config.UserSearch { } } - if auth.LDAP != nil { - userDN, err := auth.LDAP.Search(username) + if auth.ldap != nil { + userDN, err := auth.ldap.Search(username) if err != nil { log.Warn().Err(err).Str("username", username).Msg("Failed to search for user in LDAP") - return config.UserSearch{} + return config.UserSearch{ + Type: "error", + } } return config.UserSearch{ @@ -90,14 +92,14 @@ func (auth *AuthService) VerifyUser(search config.UserSearch, password string) b user := auth.GetLocalUser(search.Username) return auth.CheckPassword(user, password) case "ldap": - if auth.LDAP != nil { - err := auth.LDAP.Bind(search.Username, password) + if auth.ldap != nil { + err := auth.ldap.Bind(search.Username, password) if err != nil { log.Warn().Err(err).Str("username", search.Username).Msg("Failed to bind to LDAP") return false } - err = auth.LDAP.Bind(auth.LDAP.Config.BindDN, auth.LDAP.Config.BindPassword) + err = auth.ldap.Bind(auth.ldap.Config.BindDN, auth.ldap.Config.BindPassword) if err != nil { log.Error().Err(err).Msg("Failed to rebind with service account after user authentication") return false @@ -115,7 +117,7 @@ func (auth *AuthService) VerifyUser(search config.UserSearch, password string) b } func (auth *AuthService) GetLocalUser(username string) config.User { - for _, user := range auth.Config.Users { + for _, user := range auth.config.Users { if user.Username == username { return user } @@ -130,14 +132,14 @@ func (auth *AuthService) CheckPassword(user config.User, password string) bool { } func (auth *AuthService) IsAccountLocked(identifier string) (bool, int) { - auth.LoginMutex.RLock() - defer auth.LoginMutex.RUnlock() + auth.loginMutex.RLock() + defer auth.loginMutex.RUnlock() - if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 { + if auth.config.LoginMaxRetries <= 0 || auth.config.LoginTimeout <= 0 { return false, 0 } - attempt, exists := auth.LoginAttempts[identifier] + attempt, exists := auth.loginAttempts[identifier] if !exists { return false, 0 } @@ -151,17 +153,17 @@ func (auth *AuthService) IsAccountLocked(identifier string) (bool, int) { } func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) { - if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 { + if auth.config.LoginMaxRetries <= 0 || auth.config.LoginTimeout <= 0 { return } - auth.LoginMutex.Lock() - defer auth.LoginMutex.Unlock() + auth.loginMutex.Lock() + defer auth.loginMutex.Unlock() - attempt, exists := auth.LoginAttempts[identifier] + attempt, exists := auth.loginAttempts[identifier] if !exists { attempt = &LoginAttempt{} - auth.LoginAttempts[identifier] = attempt + auth.loginAttempts[identifier] = attempt } attempt.LastAttempt = time.Now() @@ -174,14 +176,14 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) { attempt.FailedAttempts++ - if attempt.FailedAttempts >= auth.Config.LoginMaxRetries { - attempt.LockedUntil = time.Now().Add(time.Duration(auth.Config.LoginTimeout) * time.Second) - log.Warn().Str("identifier", identifier).Int("timeout", auth.Config.LoginTimeout).Msg("Account locked due to too many failed login attempts") + if attempt.FailedAttempts >= auth.config.LoginMaxRetries { + attempt.LockedUntil = time.Now().Add(time.Duration(auth.config.LoginTimeout) * time.Second) + log.Warn().Str("identifier", identifier).Int("timeout", auth.config.LoginTimeout).Msg("Account locked due to too many failed login attempts") } } func (auth *AuthService) IsEmailWhitelisted(email string) bool { - return utils.CheckFilter(auth.Config.OauthWhitelist, email) + return utils.CheckFilter(auth.config.OauthWhitelist, email) } func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.SessionCookie) error { @@ -196,7 +198,7 @@ func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.Sessio if data.TotpPending { expiry = 3600 } else { - expiry = auth.Config.SessionExpiry + expiry = auth.config.SessionExpiry } session := model.Session{ @@ -210,37 +212,37 @@ func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.Sessio Expiry: time.Now().Add(time.Duration(expiry) * time.Second).Unix(), } - err = auth.Database.Create(&session).Error + err = auth.database.Create(&session).Error if err != nil { return err } - c.SetCookie(auth.Config.SessionCookieName, session.UUID, expiry, "/", fmt.Sprintf(".%s", auth.Config.RootDomain), auth.Config.SecureCookie, true) + c.SetCookie(auth.config.SessionCookieName, session.UUID, expiry, "/", fmt.Sprintf(".%s", auth.config.RootDomain), auth.config.SecureCookie, true) return nil } func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error { - cookie, err := c.Cookie(auth.Config.SessionCookieName) + cookie, err := c.Cookie(auth.config.SessionCookieName) if err != nil { return err } - res := auth.Database.Unscoped().Where("uuid = ?", cookie).Delete(&model.Session{}) + res := auth.database.Unscoped().Where("uuid = ?", cookie).Delete(&model.Session{}) if res.Error != nil { return res.Error } - c.SetCookie(auth.Config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.Config.RootDomain), auth.Config.SecureCookie, true) + c.SetCookie(auth.config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.config.RootDomain), auth.config.SecureCookie, true) return nil } func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie, error) { - cookie, err := c.Cookie(auth.Config.SessionCookieName) + cookie, err := c.Cookie(auth.config.SessionCookieName) if err != nil { return config.SessionCookie{}, err @@ -248,7 +250,7 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie, var session model.Session - res := auth.Database.Unscoped().Where("uuid = ?", cookie).First(&session) + res := auth.database.Unscoped().Where("uuid = ?", cookie).First(&session) if res.Error != nil { return config.SessionCookie{}, res.Error @@ -261,7 +263,7 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie, currentTime := time.Now().Unix() if currentTime > session.Expiry { - res := auth.Database.Unscoped().Where("uuid = ?", session.UUID).Delete(&model.Session{}) + res := auth.database.Unscoped().Where("uuid = ?", session.UUID).Delete(&model.Session{}) if res.Error != nil { log.Error().Err(res.Error).Msg("Failed to delete expired session") } @@ -280,7 +282,7 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie, } func (auth *AuthService) UserAuthConfigured() bool { - return len(auth.Config.Users) > 0 || auth.LDAP != nil + return len(auth.config.Users) > 0 || auth.ldap != nil } func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, labels config.AppLabels) bool { diff --git a/internal/service/database_service.go b/internal/service/database_service.go index 858ba4cc..eb75b9fc 100644 --- a/internal/service/database_service.go +++ b/internal/service/database_service.go @@ -16,18 +16,18 @@ type DatabaseServiceConfig struct { } type DatabaseService struct { - Config DatabaseServiceConfig - Database *gorm.DB + config DatabaseServiceConfig + database *gorm.DB } func NewDatabaseService(config DatabaseServiceConfig) *DatabaseService { return &DatabaseService{ - Config: config, + config: config, } } func (ds *DatabaseService) Init() error { - gormDB, err := gorm.Open(sqlite.Open(ds.Config.DatabasePath), &gorm.Config{}) + gormDB, err := gorm.Open(sqlite.Open(ds.config.DatabasePath), &gorm.Config{}) if err != nil { return err @@ -47,7 +47,7 @@ func (ds *DatabaseService) Init() error { return err } - ds.Database = gormDB + ds.database = gormDB return nil } @@ -74,5 +74,5 @@ func (ds *DatabaseService) migrateDatabase(sqlDB *sql.DB) error { } func (ds *DatabaseService) GetDatabase() *gorm.DB { - return ds.Database + return ds.database } diff --git a/internal/service/docker_service.go b/internal/service/docker_service.go index e078a7e0..f4ce2369 100644 --- a/internal/service/docker_service.go +++ b/internal/service/docker_service.go @@ -12,8 +12,8 @@ import ( ) type DockerService struct { - Client *client.Client - Context context.Context + client *client.Client + context context.Context } func NewDockerService() *DockerService { @@ -29,13 +29,13 @@ func (docker *DockerService) Init() error { ctx := context.Background() client.NegotiateAPIVersion(ctx) - docker.Client = client - docker.Context = ctx + docker.client = client + docker.context = ctx return nil } func (docker *DockerService) GetContainers() ([]container.Summary, error) { - containers, err := docker.Client.ContainerList(docker.Context, container.ListOptions{}) + containers, err := docker.client.ContainerList(docker.context, container.ListOptions{}) if err != nil { return nil, err } @@ -43,7 +43,7 @@ func (docker *DockerService) GetContainers() ([]container.Summary, error) { } func (docker *DockerService) InspectContainer(containerId string) (container.InspectResponse, error) { - inspect, err := docker.Client.ContainerInspect(docker.Context, containerId) + inspect, err := docker.client.ContainerInspect(docker.context, containerId) if err != nil { return container.InspectResponse{}, err } @@ -51,11 +51,11 @@ func (docker *DockerService) InspectContainer(containerId string) (container.Ins } func (docker *DockerService) DockerConnected() bool { - _, err := docker.Client.Ping(docker.Context) + _, err := docker.client.Ping(docker.context) return err == nil } -func (docker *DockerService) GetLabels(app string, domain string) (config.AppLabels, error) { +func (docker *DockerService) GetLabels(appDomain string) (config.AppLabels, error) { isConnected := docker.DockerConnected() if !isConnected { @@ -68,21 +68,21 @@ func (docker *DockerService) GetLabels(app string, domain string) (config.AppLab return config.AppLabels{}, err } - for _, container := range containers { - inspect, err := docker.InspectContainer(container.ID) + for _, ctr := range containers { + inspect, err := docker.InspectContainer(ctr.ID) if err != nil { - log.Warn().Str("id", container.ID).Err(err).Msg("Error inspecting container, skipping") + log.Warn().Str("id", ctr.ID).Err(err).Msg("Error inspecting container, skipping") continue } labels, err := utils.GetLabels(inspect.Config.Labels) if err != nil { - log.Warn().Str("id", container.ID).Err(err).Msg("Error getting container labels, skipping") + log.Warn().Str("id", ctr.ID).Err(err).Msg("Error getting container labels, skipping") continue } for appName, appLabels := range labels.Apps { - if appLabels.Config.Domain == domain { + if appLabels.Config.Domain == appDomain { log.Debug().Str("id", inspect.ID).Msg("Found matching container by domain") return appLabels, nil } diff --git a/internal/service/generic_oauth_service.go b/internal/service/generic_oauth_service.go index c16384db..72c23572 100644 --- a/internal/service/generic_oauth_service.go +++ b/internal/service/generic_oauth_service.go @@ -16,17 +16,17 @@ import ( ) type GenericOAuthService struct { - Config oauth2.Config - Context context.Context - Token *oauth2.Token - Verifier string - InsecureSkipVerify bool - UserinfoURL string + config oauth2.Config + context context.Context + token *oauth2.Token + verifier string + insecureSkipVerify bool + userinfoUrl string } func NewGenericOAuthService(config config.OAuthServiceConfig) *GenericOAuthService { return &GenericOAuthService{ - Config: oauth2.Config{ + config: oauth2.Config{ ClientID: config.ClientID, ClientSecret: config.ClientSecret, RedirectURL: config.RedirectURL, @@ -36,15 +36,15 @@ func NewGenericOAuthService(config config.OAuthServiceConfig) *GenericOAuthServi TokenURL: config.TokenURL, }, }, - InsecureSkipVerify: config.InsecureSkipVerify, - UserinfoURL: config.UserinfoURL, + insecureSkipVerify: config.InsecureSkipVerify, + userinfoUrl: config.UserinfoURL, } } func (generic *GenericOAuthService) Init() error { transport := &http.Transport{ TLSClientConfig: &tls.Config{ - InsecureSkipVerify: generic.InsecureSkipVerify, + InsecureSkipVerify: generic.insecureSkipVerify, MinVersion: tls.VersionTLS12, }, } @@ -58,8 +58,8 @@ func (generic *GenericOAuthService) Init() error { ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) verifier := oauth2.GenerateVerifier() - generic.Context = ctx - generic.Verifier = verifier + generic.context = ctx + generic.verifier = verifier return nil } @@ -74,26 +74,26 @@ func (generic *GenericOAuthService) GenerateState() string { } func (generic *GenericOAuthService) GetAuthURL(state string) string { - return generic.Config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(generic.Verifier)) + return generic.config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(generic.verifier)) } func (generic *GenericOAuthService) VerifyCode(code string) error { - token, err := generic.Config.Exchange(generic.Context, code, oauth2.VerifierOption(generic.Verifier)) + token, err := generic.config.Exchange(generic.context, code, oauth2.VerifierOption(generic.verifier)) if err != nil { return err } - generic.Token = token + generic.token = token return nil } func (generic *GenericOAuthService) Userinfo() (config.Claims, error) { var user config.Claims - client := generic.Config.Client(generic.Context, generic.Token) + client := generic.config.Client(generic.context, generic.token) - res, err := client.Get(generic.UserinfoURL) + res, err := client.Get(generic.userinfoUrl) if err != nil { return user, err } diff --git a/internal/service/github_oauth_service.go b/internal/service/github_oauth_service.go index 7f8466b9..26d73b13 100644 --- a/internal/service/github_oauth_service.go +++ b/internal/service/github_oauth_service.go @@ -29,15 +29,15 @@ type GithubUserInfoResponse struct { } type GithubOAuthService struct { - Config oauth2.Config - Context context.Context - Token *oauth2.Token - Verifier string + config oauth2.Config + context context.Context + token *oauth2.Token + verifier string } func NewGithubOAuthService(config config.OAuthServiceConfig) *GithubOAuthService { return &GithubOAuthService{ - Config: oauth2.Config{ + config: oauth2.Config{ ClientID: config.ClientID, ClientSecret: config.ClientSecret, RedirectURL: config.RedirectURL, @@ -53,8 +53,8 @@ func (github *GithubOAuthService) Init() error { ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) verifier := oauth2.GenerateVerifier() - github.Context = ctx - github.Verifier = verifier + github.context = ctx + github.verifier = verifier return nil } @@ -69,24 +69,24 @@ func (github *GithubOAuthService) GenerateState() string { } func (github *GithubOAuthService) GetAuthURL(state string) string { - return github.Config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(github.Verifier)) + return github.config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(github.verifier)) } func (github *GithubOAuthService) VerifyCode(code string) error { - token, err := github.Config.Exchange(github.Context, code, oauth2.VerifierOption(github.Verifier)) + token, err := github.config.Exchange(github.context, code, oauth2.VerifierOption(github.verifier)) if err != nil { return err } - github.Token = token + github.token = token return nil } func (github *GithubOAuthService) Userinfo() (config.Claims, error) { var user config.Claims - client := github.Config.Client(github.Context, github.Token) + client := github.config.Client(github.context, github.token) req, err := http.NewRequest("GET", "https://api.github.com/user", nil) if err != nil { diff --git a/internal/service/google_oauth_service.go b/internal/service/google_oauth_service.go index 1605a855..0f8c7eb8 100644 --- a/internal/service/google_oauth_service.go +++ b/internal/service/google_oauth_service.go @@ -24,15 +24,15 @@ type GoogleUserInfoResponse struct { } type GoogleOAuthService struct { - Config oauth2.Config - Context context.Context - Token *oauth2.Token - Verifier string + config oauth2.Config + context context.Context + token *oauth2.Token + verifier string } func NewGoogleOAuthService(config config.OAuthServiceConfig) *GoogleOAuthService { return &GoogleOAuthService{ - Config: oauth2.Config{ + config: oauth2.Config{ ClientID: config.ClientID, ClientSecret: config.ClientSecret, RedirectURL: config.RedirectURL, @@ -48,8 +48,8 @@ func (google *GoogleOAuthService) Init() error { ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) verifier := oauth2.GenerateVerifier() - google.Context = ctx - google.Verifier = verifier + google.context = ctx + google.verifier = verifier return nil } @@ -64,24 +64,24 @@ func (oauth *GoogleOAuthService) GenerateState() string { } func (google *GoogleOAuthService) GetAuthURL(state string) string { - return google.Config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(google.Verifier)) + return google.config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(google.verifier)) } func (google *GoogleOAuthService) VerifyCode(code string) error { - token, err := google.Config.Exchange(google.Context, code, oauth2.VerifierOption(google.Verifier)) + token, err := google.config.Exchange(google.context, code, oauth2.VerifierOption(google.verifier)) if err != nil { return err } - google.Token = token + google.token = token return nil } func (google *GoogleOAuthService) Userinfo() (config.Claims, error) { var user config.Claims - client := google.Config.Client(google.Context, google.Token) + client := google.config.Client(google.context, google.token) res, err := client.Get("https://www.googleapis.com/userinfo/v2/me") if err != nil { diff --git a/internal/service/ldap_service.go b/internal/service/ldap_service.go index b3a1d86f..5734c639 100644 --- a/internal/service/ldap_service.go +++ b/internal/service/ldap_service.go @@ -22,9 +22,9 @@ type LdapServiceConfig struct { } type LdapService struct { - Config LdapServiceConfig - Conn *ldapgo.Conn - Mutex sync.RWMutex + Config LdapServiceConfig // exported so as the auth service can use it + conn *ldapgo.Conn + mutex sync.RWMutex } func NewLdapService(config LdapServiceConfig) *LdapService { @@ -57,7 +57,8 @@ func (ldap *LdapService) Init() error { } func (ldap *LdapService) connect() (*ldapgo.Conn, error) { - ldap.Mutex.Lock() + ldap.mutex.Lock() + defer ldap.mutex.Unlock() conn, err := ldapgo.DialURL(ldap.Config.Address, ldapgo.DialWithTLSConfig(&tls.Config{ InsecureSkipVerify: ldap.Config.Insecure, @@ -72,10 +73,8 @@ func (ldap *LdapService) connect() (*ldapgo.Conn, error) { return nil, err } - ldap.Mutex.Unlock() - // Set and return the connection - ldap.Conn = conn + ldap.conn = conn return conn, nil } @@ -92,12 +91,13 @@ func (ldap *LdapService) Search(username string) (string, error) { nil, ) - ldap.Mutex.Lock() - searchResult, err := ldap.Conn.Search(searchRequest) + ldap.mutex.Lock() + defer ldap.mutex.Unlock() + + searchResult, err := ldap.conn.Search(searchRequest) if err != nil { return "", err } - ldap.Mutex.Unlock() if len(searchResult.Entries) != 1 { return "", fmt.Errorf("multiple or no entries found for user %s", username) @@ -108,12 +108,12 @@ func (ldap *LdapService) Search(username string) (string, error) { } func (ldap *LdapService) Bind(userDN string, password string) error { - ldap.Mutex.Lock() - err := ldap.Conn.Bind(userDN, password) + ldap.mutex.Lock() + defer ldap.mutex.Unlock() + err := ldap.conn.Bind(userDN, password) if err != nil { return err } - ldap.Mutex.Unlock() return nil } @@ -128,12 +128,12 @@ func (ldap *LdapService) heartbeat() error { nil, ) - ldap.Mutex.Lock() - _, err := ldap.Conn.Search(searchRequest) + ldap.mutex.Lock() + defer ldap.mutex.Unlock() + _, err := ldap.conn.Search(searchRequest) if err != nil { return err } - ldap.Mutex.Unlock() // No error means the connection is alive return nil @@ -149,7 +149,7 @@ func (ldap *LdapService) reconnect() error { exp.Reset() operation := func() (*ldapgo.Conn, error) { - ldap.Conn.Close() + ldap.conn.Close() conn, err := ldap.connect() if err != nil { return nil, err diff --git a/internal/service/oauth_broker_service.go b/internal/service/oauth_broker_service.go index 6b5b1e6f..f9df4f87 100644 --- a/internal/service/oauth_broker_service.go +++ b/internal/service/oauth_broker_service.go @@ -5,6 +5,7 @@ import ( "tinyauth/internal/config" "github.com/rs/zerolog/log" + "golang.org/x/exp/slices" ) type OAuthService interface { @@ -16,59 +17,60 @@ type OAuthService interface { } type OAuthBrokerService struct { - Services map[string]OAuthService - Configs map[string]config.OAuthServiceConfig + services map[string]OAuthService + configs map[string]config.OAuthServiceConfig } func NewOAuthBrokerService(configs map[string]config.OAuthServiceConfig) *OAuthBrokerService { return &OAuthBrokerService{ - Services: make(map[string]OAuthService), - Configs: configs, + services: make(map[string]OAuthService), + configs: configs, } } func (broker *OAuthBrokerService) Init() error { - for name, cfg := range broker.Configs { + for name, cfg := range broker.configs { switch name { case "github": service := NewGithubOAuthService(cfg) - broker.Services[name] = service + broker.services[name] = service case "google": service := NewGoogleOAuthService(cfg) - broker.Services[name] = service + broker.services[name] = service default: service := NewGenericOAuthService(cfg) - broker.Services[name] = service + broker.services[name] = service } } - for name, service := range broker.Services { + for name, service := range broker.services { err := service.Init() if err != nil { - log.Error().Err(err).Msgf("Failed to initialize OAuth service: %s", name) + log.Error().Err(err).Msgf("Failed to initialize OAuth service: %T", name) return err } - log.Info().Msgf("Initialized OAuth service: %s", name) + log.Info().Msgf("Initialized OAuth service: %T", name) } return nil } func (broker *OAuthBrokerService) GetConfiguredServices() []string { - services := make([]string, 0, len(broker.Services)) - for name := range broker.Services { + services := make([]string, 0, len(broker.services)) + for name := range broker.services { services = append(services, name) } + slices.Sort(services) return services } func (broker *OAuthBrokerService) GetService(name string) (OAuthService, bool) { - service, exists := broker.Services[name] + service, exists := broker.services[name] return service, exists } func (broker *OAuthBrokerService) GetUser(service string) (config.Claims, error) { - oauthService, exists := broker.Services[service] + oauthService, exists := broker.services[service] if !exists { return config.Claims{}, errors.New("oauth service not found") } From 9ce16c96523606f716f5fb76ef054c507f5eabb2 Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 2 Sep 2025 18:38:11 +0300 Subject: [PATCH 010/127] fix: expire csrf cookie if it's invalid --- internal/controller/oauth_controller.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/controller/oauth_controller.go b/internal/controller/oauth_controller.go index 23d00de9..ddf2d02b 100644 --- a/internal/controller/oauth_controller.go +++ b/internal/controller/oauth_controller.go @@ -108,6 +108,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { if err != nil || state != csrfCookie { log.Warn().Err(err).Msg("CSRF token mismatch or cookie missing") + c.SetCookie(controller.config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.RootDomain), controller.config.SecureCookie, true) c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) return } From f0d2da281aee1ea2b2e7baafd28521e2368fc3cf Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 2 Sep 2025 19:06:52 +0300 Subject: [PATCH 011/127] feat: header based acls (#337) * feat: add header decoder * feat: allow for dash substitute over slash for environments like kubernetes * feat: use decoded headers in proxy controller * refactor: simplify decode header to node function * refactor: use stdlib prefix check in header decoder * fix: lowercase key and filter before comparing --- internal/config/config.go | 36 +++--- internal/controller/proxy_controller.go | 40 ++++-- internal/service/auth_service.go | 8 +- internal/service/docker_service.go | 12 +- internal/utils/decoders/header_decoder.go | 119 ++++++++++++++++++ .../utils/decoders/header_decoder_test.go | 73 +++++++++++ internal/utils/decoders/label_decoder.go | 19 +++ internal/utils/decoders/label_decoder_test.go | 73 +++++++++++ .../utils/{label_utils.go => header_utils.go} | 24 ++-- internal/utils/security_utils.go | 2 + 10 files changed, 355 insertions(+), 51 deletions(-) create mode 100644 internal/utils/decoders/header_decoder.go create mode 100644 internal/utils/decoders/header_decoder_test.go create mode 100644 internal/utils/decoders/label_decoder.go create mode 100644 internal/utils/decoders/label_decoder_test.go rename internal/utils/{label_utils.go => header_utils.go} (73%) diff --git a/internal/config/config.go b/internal/config/config.go index 82050def..fbe5aa60 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -123,53 +123,53 @@ type RedirectQuery struct { RedirectURI string `url:"redirect_uri"` } -// Labels +// App config -type Labels struct { - Apps map[string]AppLabels +type AppConfigs struct { + Apps map[string]App } -type AppLabels struct { - Config ConfigLabels - Users UsersLabels - OAuth OAuthLabels - IP IPLabels - Response ResponseLabels - Path PathLabels +type App struct { + Config AppConfig + Users AppUsers + OAuth AppOAuth + IP AppIP + Response AppResponse + Path AppPath } -type ConfigLabels struct { +type AppConfig struct { Domain string } -type UsersLabels struct { +type AppUsers struct { Allow string Block string } -type OAuthLabels struct { +type AppOAuth struct { Whitelist string Groups string } -type IPLabels struct { +type AppIP struct { Allow []string Block []string Bypass []string } -type ResponseLabels struct { +type AppResponse struct { Headers []string - BasicAuth BasicAuthLabels + BasicAuth AppBasicAuth } -type BasicAuthLabels struct { +type AppBasicAuth struct { Username string Password string PasswordFile string } -type PathLabels struct { +type AppPath struct { Allow string Block string } diff --git a/internal/controller/proxy_controller.go b/internal/controller/proxy_controller.go index fd250768..dde799c8 100644 --- a/internal/controller/proxy_controller.go +++ b/internal/controller/proxy_controller.go @@ -7,6 +7,7 @@ import ( "tinyauth/internal/config" "tinyauth/internal/service" "tinyauth/internal/utils" + "tinyauth/internal/utils/decoders" "github.com/gin-gonic/gin" "github.com/google/go-querystring/query" @@ -67,6 +68,16 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { proto := c.Request.Header.Get("X-Forwarded-Proto") host := c.Request.Header.Get("X-Forwarded-Host") + var app config.App + + headers, err := decoders.DecodeHeaders(utils.NormalizeHeaders(c.Request.Header)) + + if err != nil { + log.Error().Err(err).Msg("Failed to decode headers") + controller.handleError(c, req, isBrowser) + return + } + labels, err := controller.docker.GetLabels(host) if err != nil { @@ -75,10 +86,21 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { return } + if len(headers.Apps) > 0 { + for k, v := range headers.Apps { + log.Debug().Str("app", k).Msg("Using headers for app config instead of labels") + app = v + break + } + } else { + log.Debug().Msg("No app config found in headers, using labels") + app = labels + } + clientIP := c.ClientIP() - if controller.auth.IsBypassedIP(labels.IP, clientIP) { - controller.setHeaders(c, labels) + if controller.auth.IsBypassedIP(app.IP, clientIP) { + controller.setHeaders(c, app) c.JSON(200, gin.H{ "status": 200, "message": "Authenticated", @@ -86,7 +108,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { return } - authEnabled, err := controller.auth.IsAuthEnabled(uri, labels.Path) + authEnabled, err := controller.auth.IsAuthEnabled(uri, app.Path) if err != nil { log.Error().Err(err).Msg("Failed to check if auth is enabled for resource") @@ -96,7 +118,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { if !authEnabled { log.Debug().Msg("Authentication disabled for resource, allowing access") - controller.setHeaders(c, labels) + controller.setHeaders(c, app) c.JSON(200, gin.H{ "status": 200, "message": "Authenticated", @@ -104,7 +126,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { return } - if !controller.auth.CheckIP(labels.IP, clientIP) { + if !controller.auth.CheckIP(app.IP, clientIP) { if req.Proxy == "nginx" || !isBrowser { c.JSON(401, gin.H{ "status": 401, @@ -147,7 +169,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { } if userContext.IsLoggedIn { - appAllowed := controller.auth.IsResourceAllowed(c, userContext, labels) + appAllowed := controller.auth.IsResourceAllowed(c, userContext, app) if !appAllowed { log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource") @@ -181,7 +203,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { } if userContext.OAuth { - groupOK := controller.auth.IsInOAuthGroup(c, userContext, labels.OAuth.Groups) + groupOK := controller.auth.IsInOAuthGroup(c, userContext, app.OAuth.Groups) if !groupOK { log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements") @@ -221,7 +243,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { c.Header("Remote-Email", utils.SanitizeHeader(userContext.Email)) c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups)) - controller.setHeaders(c, labels) + controller.setHeaders(c, app) c.JSON(200, gin.H{ "status": 200, @@ -251,7 +273,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/login?%s", controller.config.AppURL, queries.Encode())) } -func (controller *ProxyController) setHeaders(c *gin.Context, labels config.AppLabels) { +func (controller *ProxyController) setHeaders(c *gin.Context, labels config.App) { c.Header("Authorization", c.Request.Header.Get("Authorization")) headers := utils.ParseHeaders(labels.Response.Headers) diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index cb14a7e9..9739cb93 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -285,7 +285,7 @@ func (auth *AuthService) UserAuthConfigured() bool { return len(auth.config.Users) > 0 || auth.ldap != nil } -func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, labels config.AppLabels) bool { +func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, labels config.App) bool { if context.OAuth { log.Debug().Msg("Checking OAuth whitelist") return utils.CheckFilter(labels.OAuth.Whitelist, context.Email) @@ -322,7 +322,7 @@ func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserConte return false } -func (auth *AuthService) IsAuthEnabled(uri string, path config.PathLabels) (bool, error) { +func (auth *AuthService) IsAuthEnabled(uri string, path config.AppPath) (bool, error) { // Check for block list if path.Block != "" { regex, err := regexp.Compile(path.Block) @@ -364,7 +364,7 @@ func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User { } } -func (auth *AuthService) CheckIP(labels config.IPLabels, ip string) bool { +func (auth *AuthService) CheckIP(labels config.AppIP, ip string) bool { for _, blocked := range labels.Block { res, err := utils.FilterIP(blocked, ip) if err != nil { @@ -398,7 +398,7 @@ func (auth *AuthService) CheckIP(labels config.IPLabels, ip string) bool { return true } -func (auth *AuthService) IsBypassedIP(labels config.IPLabels, ip string) bool { +func (auth *AuthService) IsBypassedIP(labels config.AppIP, ip string) bool { for _, bypassed := range labels.Bypass { res, err := utils.FilterIP(bypassed, ip) if err != nil { diff --git a/internal/service/docker_service.go b/internal/service/docker_service.go index f4ce2369..d2a4cfc4 100644 --- a/internal/service/docker_service.go +++ b/internal/service/docker_service.go @@ -4,7 +4,7 @@ import ( "context" "strings" "tinyauth/internal/config" - "tinyauth/internal/utils" + "tinyauth/internal/utils/decoders" container "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" @@ -55,17 +55,17 @@ func (docker *DockerService) DockerConnected() bool { return err == nil } -func (docker *DockerService) GetLabels(appDomain string) (config.AppLabels, error) { +func (docker *DockerService) GetLabels(appDomain string) (config.App, error) { isConnected := docker.DockerConnected() if !isConnected { log.Debug().Msg("Docker not connected, returning empty labels") - return config.AppLabels{}, nil + return config.App{}, nil } containers, err := docker.GetContainers() if err != nil { - return config.AppLabels{}, err + return config.App{}, err } for _, ctr := range containers { @@ -75,7 +75,7 @@ func (docker *DockerService) GetLabels(appDomain string) (config.AppLabels, erro continue } - labels, err := utils.GetLabels(inspect.Config.Labels) + labels, err := decoders.DecodeLabels(inspect.Config.Labels) if err != nil { log.Warn().Str("id", ctr.ID).Err(err).Msg("Error getting container labels, skipping") continue @@ -95,5 +95,5 @@ func (docker *DockerService) GetLabels(appDomain string) (config.AppLabels, erro } log.Debug().Msg("No matching container found, returning empty labels") - return config.AppLabels{}, nil + return config.App{}, nil } diff --git a/internal/utils/decoders/header_decoder.go b/internal/utils/decoders/header_decoder.go new file mode 100644 index 00000000..834b8451 --- /dev/null +++ b/internal/utils/decoders/header_decoder.go @@ -0,0 +1,119 @@ +package decoders + +import ( + "fmt" + "sort" + "strings" + "tinyauth/internal/config" + + "github.com/traefik/paerser/parser" +) + +// Based on: https://github.com/traefik/paerser/blob/master/parser/labels_decode.go (Apache 2.0 License) + +func DecodeHeaders(headers map[string]string) (config.AppConfigs, error) { + var app config.AppConfigs + + err := decodeHeadersHelper(headers, &app, "tinyauth", "tinyauth-apps") + + if err != nil { + return config.AppConfigs{}, err + } + + return app, nil +} + +func decodeHeadersHelper(headers map[string]string, element any, rootName string, filters ...string) error { + node, err := decodeHeadersToNode(headers, rootName, filters...) + + if err != nil { + return err + } + + opts := parser.MetadataOpts{TagName: "header", AllowSliceAsStruct: true} + err = parser.AddMetadata(element, node, opts) + + if err != nil { + return err + } + + return parser.Fill(element, node, parser.FillerOpts{AllowSliceAsStruct: true}) +} + +func decodeHeadersToNode(headers map[string]string, rootName string, filters ...string) (*parser.Node, error) { + sortedKeys := sortKeys(headers, filters) + + var node *parser.Node + + for i, key := range sortedKeys { + split := strings.Split(strings.ToLower(key), "-") + + if split[0] != rootName { + return nil, fmt.Errorf("invalid header root %s", split[0]) + } + + for _, v := range split { + if v == "" { + return nil, fmt.Errorf("invalid element: %s", key) + } + } + + if i == 0 { + node = &parser.Node{} + } + + decodeHeaderToNode(node, split, headers[key]) + } + + return node, nil +} + +func decodeHeaderToNode(root *parser.Node, path []string, value string) { + if len(root.Name) == 0 { + root.Name = path[0] + } + + if len(path) > 1 { + node := containsNode(root.Children, path[1]) + + if node != nil { + decodeHeaderToNode(node, path[1:], value) + } else { + child := &parser.Node{Name: path[1]} + decodeHeaderToNode(child, path[1:], value) + root.Children = append(root.Children, child) + } + } else { + root.Value = value + } +} + +func containsNode(nodes []*parser.Node, name string) *parser.Node { + for _, node := range nodes { + if strings.EqualFold(node.Name, name) { + return node + } + } + return nil +} + +func sortKeys(headers map[string]string, filters []string) []string { + var sortedKeys []string + + for key := range headers { + if len(filters) == 0 { + sortedKeys = append(sortedKeys, key) + continue + } + + for _, filter := range filters { + if strings.HasPrefix(strings.ToLower(key), strings.ToLower(filter)) { + sortedKeys = append(sortedKeys, key) + continue + } + } + } + + sort.Strings(sortedKeys) + return sortedKeys +} diff --git a/internal/utils/decoders/header_decoder_test.go b/internal/utils/decoders/header_decoder_test.go new file mode 100644 index 00000000..02d09900 --- /dev/null +++ b/internal/utils/decoders/header_decoder_test.go @@ -0,0 +1,73 @@ +package decoders_test + +import ( + "reflect" + "testing" + "tinyauth/internal/config" + "tinyauth/internal/utils/decoders" +) + +func TestDecodeHeaders(t *testing.T) { + // Variables + expected := config.AppConfigs{ + Apps: map[string]config.App{ + "foo": { + Config: config.AppConfig{ + Domain: "example.com", + }, + Users: config.AppUsers{ + Allow: "user1,user2", + Block: "user3", + }, + OAuth: config.AppOAuth{ + Whitelist: "somebody@example.com", + Groups: "group3", + }, + IP: config.AppIP{ + Allow: []string{"10.71.0.1/24", "10.71.0.2"}, + Block: []string{"10.10.10.10", "10.0.0.0/24"}, + Bypass: []string{"192.168.1.1"}, + }, + Response: config.AppResponse{ + Headers: []string{"X-Foo=Bar", "X-Baz=Qux"}, + BasicAuth: config.AppBasicAuth{ + Username: "admin", + Password: "password", + PasswordFile: "/path/to/passwordfile", + }, + }, + Path: config.AppPath{ + Allow: "/public", + Block: "/private", + }, + }, + }, + } + test := map[string]string{ + "Tinyauth-Apps-Foo-Config-Domain": "example.com", + "Tinyauth-Apps-Foo-Users-Allow": "user1,user2", + "Tinyauth-Apps-Foo-Users-Block": "user3", + "Tinyauth-Apps-Foo-OAuth-Whitelist": "somebody@example.com", + "Tinyauth-Apps-Foo-OAuth-Groups": "group3", + "Tinyauth-Apps-Foo-IP-Allow": "10.71.0.1/24,10.71.0.2", + "Tinyauth-Apps-Foo-IP-Block": "10.10.10.10,10.0.0.0/24", + "Tinyauth-Apps-Foo-IP-Bypass": "192.168.1.1", + "Tinyauth-Apps-Foo-Response-Headers": "X-Foo=Bar,X-Baz=Qux", + "Tinyauth-Apps-Foo-Response-BasicAuth-Username": "admin", + "Tinyauth-Apps-Foo-Response-BasicAuth-Password": "password", + "Tinyauth-Apps-Foo-Response-BasicAuth-PasswordFile": "/path/to/passwordfile", + "Tinyauth-Apps-Foo-Path-Allow": "/public", + "Tinyauth-Apps-Foo-Path-Block": "/private", + } + + // Test + result, err := decoders.DecodeHeaders(test) + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if !reflect.DeepEqual(expected, result) { + t.Fatalf("Expected %v but got %v", expected, result) + } +} diff --git a/internal/utils/decoders/label_decoder.go b/internal/utils/decoders/label_decoder.go new file mode 100644 index 00000000..fbb7ecbf --- /dev/null +++ b/internal/utils/decoders/label_decoder.go @@ -0,0 +1,19 @@ +package decoders + +import ( + "tinyauth/internal/config" + + "github.com/traefik/paerser/parser" +) + +func DecodeLabels(labels map[string]string) (config.AppConfigs, error) { + var appLabels config.AppConfigs + + err := parser.Decode(labels, &appLabels, "tinyauth", "tinyauth.apps") + + if err != nil { + return config.AppConfigs{}, err + } + + return appLabels, nil +} diff --git a/internal/utils/decoders/label_decoder_test.go b/internal/utils/decoders/label_decoder_test.go new file mode 100644 index 00000000..558860a2 --- /dev/null +++ b/internal/utils/decoders/label_decoder_test.go @@ -0,0 +1,73 @@ +package decoders_test + +import ( + "reflect" + "testing" + "tinyauth/internal/config" + "tinyauth/internal/utils/decoders" +) + +func TestDecodeLabels(t *testing.T) { + // Variables + expected := config.AppConfigs{ + Apps: map[string]config.App{ + "foo": { + Config: config.AppConfig{ + Domain: "example.com", + }, + Users: config.AppUsers{ + Allow: "user1,user2", + Block: "user3", + }, + OAuth: config.AppOAuth{ + Whitelist: "somebody@example.com", + Groups: "group3", + }, + IP: config.AppIP{ + Allow: []string{"10.71.0.1/24", "10.71.0.2"}, + Block: []string{"10.10.10.10", "10.0.0.0/24"}, + Bypass: []string{"192.168.1.1"}, + }, + Response: config.AppResponse{ + Headers: []string{"X-Foo=Bar", "X-Baz=Qux"}, + BasicAuth: config.AppBasicAuth{ + Username: "admin", + Password: "password", + PasswordFile: "/path/to/passwordfile", + }, + }, + Path: config.AppPath{ + Allow: "/public", + Block: "/private", + }, + }, + }, + } + test := map[string]string{ + "tinyauth.apps.foo.config.domain": "example.com", + "tinyauth.apps.foo.users.allow": "user1,user2", + "tinyauth.apps.foo.users.block": "user3", + "tinyauth.apps.foo.oauth.whitelist": "somebody@example.com", + "tinyauth.apps.foo.oauth.groups": "group3", + "tinyauth.apps.foo.ip.allow": "10.71.0.1/24,10.71.0.2", + "tinyauth.apps.foo.ip.block": "10.10.10.10,10.0.0.0/24", + "tinyauth.apps.foo.ip.bypass": "192.168.1.1", + "tinyauth.apps.foo.response.headers": "X-Foo=Bar,X-Baz=Qux", + "tinyauth.apps.foo.response.basicauth.username": "admin", + "tinyauth.apps.foo.response.basicauth.password": "password", + "tinyauth.apps.foo.response.basicauth.passwordfile": "/path/to/passwordfile", + "tinyauth.apps.foo.path.allow": "/public", + "tinyauth.apps.foo.path.block": "/private", + } + + // Test + result, err := decoders.DecodeLabels(test) + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if reflect.DeepEqual(expected, result) == false { + t.Fatalf("Expected %v but got %v", expected, result) + } +} diff --git a/internal/utils/label_utils.go b/internal/utils/header_utils.go similarity index 73% rename from internal/utils/label_utils.go rename to internal/utils/header_utils.go index 5e423f7a..79aeb333 100644 --- a/internal/utils/label_utils.go +++ b/internal/utils/header_utils.go @@ -3,22 +3,8 @@ package utils import ( "net/http" "strings" - "tinyauth/internal/config" - - "github.com/traefik/paerser/parser" ) -func GetLabels(labels map[string]string) (config.Labels, error) { - var labelsParsed config.Labels - - err := parser.Decode(labels, &labelsParsed, "tinyauth", "tinyauth.apps") - if err != nil { - return config.Labels{}, err - } - - return labelsParsed, nil -} - func ParseHeaders(headers []string) map[string]string { headerMap := make(map[string]string) for _, header := range headers { @@ -46,3 +32,13 @@ func SanitizeHeader(header string) string { return -1 }, header) } + +func NormalizeHeaders(headers http.Header) map[string]string { + var result = make(map[string]string) + + for key, values := range headers { + result[key] = strings.Join(values, ",") + } + + return result +} diff --git a/internal/utils/security_utils.go b/internal/utils/security_utils.go index b40c56ca..e1f1b2ec 100644 --- a/internal/utils/security_utils.go +++ b/internal/utils/security_utils.go @@ -46,6 +46,8 @@ func GetBasicAuth(username string, password string) string { } func FilterIP(filter string, ip string) (bool, error) { + filter = strings.Replace(filter, "-", "/", -1) + ipAddr := net.ParseIP(ip) if strings.Contains(filter, "/") { From f3eb7f69b464db3a4b77fbb872626fc9a6bf9040 Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 3 Sep 2025 12:12:18 +0300 Subject: [PATCH 012/127] Revert "feat: header based acls (#337)" (#340) This reverts commit f0d2da281aee1ea2b2e7baafd28521e2368fc3cf. --- internal/config/config.go | 36 +++--- internal/controller/proxy_controller.go | 40 ++---- internal/service/auth_service.go | 8 +- internal/service/docker_service.go | 12 +- internal/utils/decoders/header_decoder.go | 119 ------------------ .../utils/decoders/header_decoder_test.go | 73 ----------- internal/utils/decoders/label_decoder.go | 19 --- internal/utils/decoders/label_decoder_test.go | 73 ----------- .../utils/{header_utils.go => label_utils.go} | 24 ++-- internal/utils/security_utils.go | 2 - 10 files changed, 51 insertions(+), 355 deletions(-) delete mode 100644 internal/utils/decoders/header_decoder.go delete mode 100644 internal/utils/decoders/header_decoder_test.go delete mode 100644 internal/utils/decoders/label_decoder.go delete mode 100644 internal/utils/decoders/label_decoder_test.go rename internal/utils/{header_utils.go => label_utils.go} (73%) diff --git a/internal/config/config.go b/internal/config/config.go index fbe5aa60..82050def 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -123,53 +123,53 @@ type RedirectQuery struct { RedirectURI string `url:"redirect_uri"` } -// App config +// Labels -type AppConfigs struct { - Apps map[string]App +type Labels struct { + Apps map[string]AppLabels } -type App struct { - Config AppConfig - Users AppUsers - OAuth AppOAuth - IP AppIP - Response AppResponse - Path AppPath +type AppLabels struct { + Config ConfigLabels + Users UsersLabels + OAuth OAuthLabels + IP IPLabels + Response ResponseLabels + Path PathLabels } -type AppConfig struct { +type ConfigLabels struct { Domain string } -type AppUsers struct { +type UsersLabels struct { Allow string Block string } -type AppOAuth struct { +type OAuthLabels struct { Whitelist string Groups string } -type AppIP struct { +type IPLabels struct { Allow []string Block []string Bypass []string } -type AppResponse struct { +type ResponseLabels struct { Headers []string - BasicAuth AppBasicAuth + BasicAuth BasicAuthLabels } -type AppBasicAuth struct { +type BasicAuthLabels struct { Username string Password string PasswordFile string } -type AppPath struct { +type PathLabels struct { Allow string Block string } diff --git a/internal/controller/proxy_controller.go b/internal/controller/proxy_controller.go index dde799c8..fd250768 100644 --- a/internal/controller/proxy_controller.go +++ b/internal/controller/proxy_controller.go @@ -7,7 +7,6 @@ import ( "tinyauth/internal/config" "tinyauth/internal/service" "tinyauth/internal/utils" - "tinyauth/internal/utils/decoders" "github.com/gin-gonic/gin" "github.com/google/go-querystring/query" @@ -68,16 +67,6 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { proto := c.Request.Header.Get("X-Forwarded-Proto") host := c.Request.Header.Get("X-Forwarded-Host") - var app config.App - - headers, err := decoders.DecodeHeaders(utils.NormalizeHeaders(c.Request.Header)) - - if err != nil { - log.Error().Err(err).Msg("Failed to decode headers") - controller.handleError(c, req, isBrowser) - return - } - labels, err := controller.docker.GetLabels(host) if err != nil { @@ -86,21 +75,10 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { return } - if len(headers.Apps) > 0 { - for k, v := range headers.Apps { - log.Debug().Str("app", k).Msg("Using headers for app config instead of labels") - app = v - break - } - } else { - log.Debug().Msg("No app config found in headers, using labels") - app = labels - } - clientIP := c.ClientIP() - if controller.auth.IsBypassedIP(app.IP, clientIP) { - controller.setHeaders(c, app) + if controller.auth.IsBypassedIP(labels.IP, clientIP) { + controller.setHeaders(c, labels) c.JSON(200, gin.H{ "status": 200, "message": "Authenticated", @@ -108,7 +86,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { return } - authEnabled, err := controller.auth.IsAuthEnabled(uri, app.Path) + authEnabled, err := controller.auth.IsAuthEnabled(uri, labels.Path) if err != nil { log.Error().Err(err).Msg("Failed to check if auth is enabled for resource") @@ -118,7 +96,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { if !authEnabled { log.Debug().Msg("Authentication disabled for resource, allowing access") - controller.setHeaders(c, app) + controller.setHeaders(c, labels) c.JSON(200, gin.H{ "status": 200, "message": "Authenticated", @@ -126,7 +104,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { return } - if !controller.auth.CheckIP(app.IP, clientIP) { + if !controller.auth.CheckIP(labels.IP, clientIP) { if req.Proxy == "nginx" || !isBrowser { c.JSON(401, gin.H{ "status": 401, @@ -169,7 +147,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { } if userContext.IsLoggedIn { - appAllowed := controller.auth.IsResourceAllowed(c, userContext, app) + appAllowed := controller.auth.IsResourceAllowed(c, userContext, labels) if !appAllowed { log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource") @@ -203,7 +181,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { } if userContext.OAuth { - groupOK := controller.auth.IsInOAuthGroup(c, userContext, app.OAuth.Groups) + groupOK := controller.auth.IsInOAuthGroup(c, userContext, labels.OAuth.Groups) if !groupOK { log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements") @@ -243,7 +221,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { c.Header("Remote-Email", utils.SanitizeHeader(userContext.Email)) c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups)) - controller.setHeaders(c, app) + controller.setHeaders(c, labels) c.JSON(200, gin.H{ "status": 200, @@ -273,7 +251,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/login?%s", controller.config.AppURL, queries.Encode())) } -func (controller *ProxyController) setHeaders(c *gin.Context, labels config.App) { +func (controller *ProxyController) setHeaders(c *gin.Context, labels config.AppLabels) { c.Header("Authorization", c.Request.Header.Get("Authorization")) headers := utils.ParseHeaders(labels.Response.Headers) diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index 9739cb93..cb14a7e9 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -285,7 +285,7 @@ func (auth *AuthService) UserAuthConfigured() bool { return len(auth.config.Users) > 0 || auth.ldap != nil } -func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, labels config.App) bool { +func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, labels config.AppLabels) bool { if context.OAuth { log.Debug().Msg("Checking OAuth whitelist") return utils.CheckFilter(labels.OAuth.Whitelist, context.Email) @@ -322,7 +322,7 @@ func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserConte return false } -func (auth *AuthService) IsAuthEnabled(uri string, path config.AppPath) (bool, error) { +func (auth *AuthService) IsAuthEnabled(uri string, path config.PathLabels) (bool, error) { // Check for block list if path.Block != "" { regex, err := regexp.Compile(path.Block) @@ -364,7 +364,7 @@ func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User { } } -func (auth *AuthService) CheckIP(labels config.AppIP, ip string) bool { +func (auth *AuthService) CheckIP(labels config.IPLabels, ip string) bool { for _, blocked := range labels.Block { res, err := utils.FilterIP(blocked, ip) if err != nil { @@ -398,7 +398,7 @@ func (auth *AuthService) CheckIP(labels config.AppIP, ip string) bool { return true } -func (auth *AuthService) IsBypassedIP(labels config.AppIP, ip string) bool { +func (auth *AuthService) IsBypassedIP(labels config.IPLabels, ip string) bool { for _, bypassed := range labels.Bypass { res, err := utils.FilterIP(bypassed, ip) if err != nil { diff --git a/internal/service/docker_service.go b/internal/service/docker_service.go index d2a4cfc4..f4ce2369 100644 --- a/internal/service/docker_service.go +++ b/internal/service/docker_service.go @@ -4,7 +4,7 @@ import ( "context" "strings" "tinyauth/internal/config" - "tinyauth/internal/utils/decoders" + "tinyauth/internal/utils" container "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" @@ -55,17 +55,17 @@ func (docker *DockerService) DockerConnected() bool { return err == nil } -func (docker *DockerService) GetLabels(appDomain string) (config.App, error) { +func (docker *DockerService) GetLabels(appDomain string) (config.AppLabels, error) { isConnected := docker.DockerConnected() if !isConnected { log.Debug().Msg("Docker not connected, returning empty labels") - return config.App{}, nil + return config.AppLabels{}, nil } containers, err := docker.GetContainers() if err != nil { - return config.App{}, err + return config.AppLabels{}, err } for _, ctr := range containers { @@ -75,7 +75,7 @@ func (docker *DockerService) GetLabels(appDomain string) (config.App, error) { continue } - labels, err := decoders.DecodeLabels(inspect.Config.Labels) + labels, err := utils.GetLabels(inspect.Config.Labels) if err != nil { log.Warn().Str("id", ctr.ID).Err(err).Msg("Error getting container labels, skipping") continue @@ -95,5 +95,5 @@ func (docker *DockerService) GetLabels(appDomain string) (config.App, error) { } log.Debug().Msg("No matching container found, returning empty labels") - return config.App{}, nil + return config.AppLabels{}, nil } diff --git a/internal/utils/decoders/header_decoder.go b/internal/utils/decoders/header_decoder.go deleted file mode 100644 index 834b8451..00000000 --- a/internal/utils/decoders/header_decoder.go +++ /dev/null @@ -1,119 +0,0 @@ -package decoders - -import ( - "fmt" - "sort" - "strings" - "tinyauth/internal/config" - - "github.com/traefik/paerser/parser" -) - -// Based on: https://github.com/traefik/paerser/blob/master/parser/labels_decode.go (Apache 2.0 License) - -func DecodeHeaders(headers map[string]string) (config.AppConfigs, error) { - var app config.AppConfigs - - err := decodeHeadersHelper(headers, &app, "tinyauth", "tinyauth-apps") - - if err != nil { - return config.AppConfigs{}, err - } - - return app, nil -} - -func decodeHeadersHelper(headers map[string]string, element any, rootName string, filters ...string) error { - node, err := decodeHeadersToNode(headers, rootName, filters...) - - if err != nil { - return err - } - - opts := parser.MetadataOpts{TagName: "header", AllowSliceAsStruct: true} - err = parser.AddMetadata(element, node, opts) - - if err != nil { - return err - } - - return parser.Fill(element, node, parser.FillerOpts{AllowSliceAsStruct: true}) -} - -func decodeHeadersToNode(headers map[string]string, rootName string, filters ...string) (*parser.Node, error) { - sortedKeys := sortKeys(headers, filters) - - var node *parser.Node - - for i, key := range sortedKeys { - split := strings.Split(strings.ToLower(key), "-") - - if split[0] != rootName { - return nil, fmt.Errorf("invalid header root %s", split[0]) - } - - for _, v := range split { - if v == "" { - return nil, fmt.Errorf("invalid element: %s", key) - } - } - - if i == 0 { - node = &parser.Node{} - } - - decodeHeaderToNode(node, split, headers[key]) - } - - return node, nil -} - -func decodeHeaderToNode(root *parser.Node, path []string, value string) { - if len(root.Name) == 0 { - root.Name = path[0] - } - - if len(path) > 1 { - node := containsNode(root.Children, path[1]) - - if node != nil { - decodeHeaderToNode(node, path[1:], value) - } else { - child := &parser.Node{Name: path[1]} - decodeHeaderToNode(child, path[1:], value) - root.Children = append(root.Children, child) - } - } else { - root.Value = value - } -} - -func containsNode(nodes []*parser.Node, name string) *parser.Node { - for _, node := range nodes { - if strings.EqualFold(node.Name, name) { - return node - } - } - return nil -} - -func sortKeys(headers map[string]string, filters []string) []string { - var sortedKeys []string - - for key := range headers { - if len(filters) == 0 { - sortedKeys = append(sortedKeys, key) - continue - } - - for _, filter := range filters { - if strings.HasPrefix(strings.ToLower(key), strings.ToLower(filter)) { - sortedKeys = append(sortedKeys, key) - continue - } - } - } - - sort.Strings(sortedKeys) - return sortedKeys -} diff --git a/internal/utils/decoders/header_decoder_test.go b/internal/utils/decoders/header_decoder_test.go deleted file mode 100644 index 02d09900..00000000 --- a/internal/utils/decoders/header_decoder_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package decoders_test - -import ( - "reflect" - "testing" - "tinyauth/internal/config" - "tinyauth/internal/utils/decoders" -) - -func TestDecodeHeaders(t *testing.T) { - // Variables - expected := config.AppConfigs{ - Apps: map[string]config.App{ - "foo": { - Config: config.AppConfig{ - Domain: "example.com", - }, - Users: config.AppUsers{ - Allow: "user1,user2", - Block: "user3", - }, - OAuth: config.AppOAuth{ - Whitelist: "somebody@example.com", - Groups: "group3", - }, - IP: config.AppIP{ - Allow: []string{"10.71.0.1/24", "10.71.0.2"}, - Block: []string{"10.10.10.10", "10.0.0.0/24"}, - Bypass: []string{"192.168.1.1"}, - }, - Response: config.AppResponse{ - Headers: []string{"X-Foo=Bar", "X-Baz=Qux"}, - BasicAuth: config.AppBasicAuth{ - Username: "admin", - Password: "password", - PasswordFile: "/path/to/passwordfile", - }, - }, - Path: config.AppPath{ - Allow: "/public", - Block: "/private", - }, - }, - }, - } - test := map[string]string{ - "Tinyauth-Apps-Foo-Config-Domain": "example.com", - "Tinyauth-Apps-Foo-Users-Allow": "user1,user2", - "Tinyauth-Apps-Foo-Users-Block": "user3", - "Tinyauth-Apps-Foo-OAuth-Whitelist": "somebody@example.com", - "Tinyauth-Apps-Foo-OAuth-Groups": "group3", - "Tinyauth-Apps-Foo-IP-Allow": "10.71.0.1/24,10.71.0.2", - "Tinyauth-Apps-Foo-IP-Block": "10.10.10.10,10.0.0.0/24", - "Tinyauth-Apps-Foo-IP-Bypass": "192.168.1.1", - "Tinyauth-Apps-Foo-Response-Headers": "X-Foo=Bar,X-Baz=Qux", - "Tinyauth-Apps-Foo-Response-BasicAuth-Username": "admin", - "Tinyauth-Apps-Foo-Response-BasicAuth-Password": "password", - "Tinyauth-Apps-Foo-Response-BasicAuth-PasswordFile": "/path/to/passwordfile", - "Tinyauth-Apps-Foo-Path-Allow": "/public", - "Tinyauth-Apps-Foo-Path-Block": "/private", - } - - // Test - result, err := decoders.DecodeHeaders(test) - - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - - if !reflect.DeepEqual(expected, result) { - t.Fatalf("Expected %v but got %v", expected, result) - } -} diff --git a/internal/utils/decoders/label_decoder.go b/internal/utils/decoders/label_decoder.go deleted file mode 100644 index fbb7ecbf..00000000 --- a/internal/utils/decoders/label_decoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package decoders - -import ( - "tinyauth/internal/config" - - "github.com/traefik/paerser/parser" -) - -func DecodeLabels(labels map[string]string) (config.AppConfigs, error) { - var appLabels config.AppConfigs - - err := parser.Decode(labels, &appLabels, "tinyauth", "tinyauth.apps") - - if err != nil { - return config.AppConfigs{}, err - } - - return appLabels, nil -} diff --git a/internal/utils/decoders/label_decoder_test.go b/internal/utils/decoders/label_decoder_test.go deleted file mode 100644 index 558860a2..00000000 --- a/internal/utils/decoders/label_decoder_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package decoders_test - -import ( - "reflect" - "testing" - "tinyauth/internal/config" - "tinyauth/internal/utils/decoders" -) - -func TestDecodeLabels(t *testing.T) { - // Variables - expected := config.AppConfigs{ - Apps: map[string]config.App{ - "foo": { - Config: config.AppConfig{ - Domain: "example.com", - }, - Users: config.AppUsers{ - Allow: "user1,user2", - Block: "user3", - }, - OAuth: config.AppOAuth{ - Whitelist: "somebody@example.com", - Groups: "group3", - }, - IP: config.AppIP{ - Allow: []string{"10.71.0.1/24", "10.71.0.2"}, - Block: []string{"10.10.10.10", "10.0.0.0/24"}, - Bypass: []string{"192.168.1.1"}, - }, - Response: config.AppResponse{ - Headers: []string{"X-Foo=Bar", "X-Baz=Qux"}, - BasicAuth: config.AppBasicAuth{ - Username: "admin", - Password: "password", - PasswordFile: "/path/to/passwordfile", - }, - }, - Path: config.AppPath{ - Allow: "/public", - Block: "/private", - }, - }, - }, - } - test := map[string]string{ - "tinyauth.apps.foo.config.domain": "example.com", - "tinyauth.apps.foo.users.allow": "user1,user2", - "tinyauth.apps.foo.users.block": "user3", - "tinyauth.apps.foo.oauth.whitelist": "somebody@example.com", - "tinyauth.apps.foo.oauth.groups": "group3", - "tinyauth.apps.foo.ip.allow": "10.71.0.1/24,10.71.0.2", - "tinyauth.apps.foo.ip.block": "10.10.10.10,10.0.0.0/24", - "tinyauth.apps.foo.ip.bypass": "192.168.1.1", - "tinyauth.apps.foo.response.headers": "X-Foo=Bar,X-Baz=Qux", - "tinyauth.apps.foo.response.basicauth.username": "admin", - "tinyauth.apps.foo.response.basicauth.password": "password", - "tinyauth.apps.foo.response.basicauth.passwordfile": "/path/to/passwordfile", - "tinyauth.apps.foo.path.allow": "/public", - "tinyauth.apps.foo.path.block": "/private", - } - - // Test - result, err := decoders.DecodeLabels(test) - - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - - if reflect.DeepEqual(expected, result) == false { - t.Fatalf("Expected %v but got %v", expected, result) - } -} diff --git a/internal/utils/header_utils.go b/internal/utils/label_utils.go similarity index 73% rename from internal/utils/header_utils.go rename to internal/utils/label_utils.go index 79aeb333..5e423f7a 100644 --- a/internal/utils/header_utils.go +++ b/internal/utils/label_utils.go @@ -3,8 +3,22 @@ package utils import ( "net/http" "strings" + "tinyauth/internal/config" + + "github.com/traefik/paerser/parser" ) +func GetLabels(labels map[string]string) (config.Labels, error) { + var labelsParsed config.Labels + + err := parser.Decode(labels, &labelsParsed, "tinyauth", "tinyauth.apps") + if err != nil { + return config.Labels{}, err + } + + return labelsParsed, nil +} + func ParseHeaders(headers []string) map[string]string { headerMap := make(map[string]string) for _, header := range headers { @@ -32,13 +46,3 @@ func SanitizeHeader(header string) string { return -1 }, header) } - -func NormalizeHeaders(headers http.Header) map[string]string { - var result = make(map[string]string) - - for key, values := range headers { - result[key] = strings.Join(values, ",") - } - - return result -} diff --git a/internal/utils/security_utils.go b/internal/utils/security_utils.go index e1f1b2ec..b40c56ca 100644 --- a/internal/utils/security_utils.go +++ b/internal/utils/security_utils.go @@ -46,8 +46,6 @@ func GetBasicAuth(username string, password string) string { } func FilterIP(filter string, ip string) (bool, error) { - filter = strings.Replace(filter, "-", "/", -1) - ipAddr := net.ParseIP(ip) if strings.Contains(filter, "/") { From 773cd6d171099c5631a43008c7bb0967b0b2e204 Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 3 Sep 2025 12:14:04 +0300 Subject: [PATCH 013/127] feat: add trusted proxies config value --- cmd/root.go | 1 + internal/bootstrap/app_bootstrap.go | 1 + internal/config/config.go | 1 + 3 files changed, 3 insertions(+) diff --git a/cmd/root.go b/cmd/root.go index 171e0433..155ccd2f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -112,6 +112,7 @@ func init() { {"ldap-search-filter", "(uid=%s)", "LDAP search filter for user lookup."}, {"resources-dir", "/data/resources", "Path to a directory containing custom resources (e.g. background image)."}, {"database-path", "/data/tinyauth.db", "Path to the Sqlite database file."}, + {"trusted-proxies", "", "Comma separated list of trusted proxies (IP addresses) for correct client IP detection and for header ACLs."}, } for _, opt := range configOptions { diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index d2ac1b0a..0f967dff 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -146,6 +146,7 @@ func (app *BootstrapApp) Setup() error { // Create engine engine := gin.New() + engine.SetTrustedProxies(strings.Split(app.Config.TrustedProxies, ",")) if config.Version != "development" { gin.SetMode(gin.ReleaseMode) diff --git a/internal/config/config.go b/internal/config/config.go index 82050def..f698746b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -53,6 +53,7 @@ type Config struct { LdapSearchFilter string `mapstructure:"ldap-search-filter"` ResourcesDir string `mapstructure:"resources-dir"` DatabasePath string `mapstructure:"database-path" validate:"required"` + TrustedProxies string `mapstructure:"trusted-proxies"` } // OAuth/OIDC config From b024d5ffda22c8affe5b17e5d8045344235e8ec6 Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 3 Sep 2025 12:16:06 +0300 Subject: [PATCH 014/127] feat: allow for dash substitute in ip filters for environments like kubernetes --- internal/utils/security_utils.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/utils/security_utils.go b/internal/utils/security_utils.go index b40c56ca..85a359db 100644 --- a/internal/utils/security_utils.go +++ b/internal/utils/security_utils.go @@ -73,6 +73,8 @@ func CheckFilter(filter string, str string) bool { return true } + filter = strings.Replace(filter, "-", "/", -1) + if strings.HasPrefix(filter, "/") && strings.HasSuffix(filter, "/") { re, err := regexp.Compile(filter[1 : len(filter)-1]) if err != nil { From f5ac7eff99009edd1b80a1403049fec55f2e87c8 Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 3 Sep 2025 12:23:21 +0300 Subject: [PATCH 015/127] refactor: mode label decoder to separate package --- internal/config/config.go | 34 ++++----- internal/controller/proxy_controller.go | 2 +- internal/service/auth_service.go | 8 +- internal/service/docker_service.go | 12 +-- internal/utils/decoders/label_decoder.go | 19 +++++ internal/utils/decoders/label_decoder_test.go | 73 +++++++++++++++++++ internal/utils/label_utils.go | 14 ---- 7 files changed, 120 insertions(+), 42 deletions(-) create mode 100644 internal/utils/decoders/label_decoder.go create mode 100644 internal/utils/decoders/label_decoder_test.go diff --git a/internal/config/config.go b/internal/config/config.go index f698746b..7ccedd3b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -126,51 +126,51 @@ type RedirectQuery struct { // Labels -type Labels struct { - Apps map[string]AppLabels +type Apps struct { + Apps map[string]App } -type AppLabels struct { - Config ConfigLabels - Users UsersLabels - OAuth OAuthLabels - IP IPLabels - Response ResponseLabels - Path PathLabels +type App struct { + Config AppConfig + Users AppUsers + OAuth AppOAuth + IP AppIP + Response AppResponse + Path AppPath } -type ConfigLabels struct { +type AppConfig struct { Domain string } -type UsersLabels struct { +type AppUsers struct { Allow string Block string } -type OAuthLabels struct { +type AppOAuth struct { Whitelist string Groups string } -type IPLabels struct { +type AppIP struct { Allow []string Block []string Bypass []string } -type ResponseLabels struct { +type AppResponse struct { Headers []string - BasicAuth BasicAuthLabels + BasicAuth AppBasicAuth } -type BasicAuthLabels struct { +type AppBasicAuth struct { Username string Password string PasswordFile string } -type PathLabels struct { +type AppPath struct { Allow string Block string } diff --git a/internal/controller/proxy_controller.go b/internal/controller/proxy_controller.go index fd250768..88eeb4dc 100644 --- a/internal/controller/proxy_controller.go +++ b/internal/controller/proxy_controller.go @@ -251,7 +251,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/login?%s", controller.config.AppURL, queries.Encode())) } -func (controller *ProxyController) setHeaders(c *gin.Context, labels config.AppLabels) { +func (controller *ProxyController) setHeaders(c *gin.Context, labels config.App) { c.Header("Authorization", c.Request.Header.Get("Authorization")) headers := utils.ParseHeaders(labels.Response.Headers) diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index cb14a7e9..9739cb93 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -285,7 +285,7 @@ func (auth *AuthService) UserAuthConfigured() bool { return len(auth.config.Users) > 0 || auth.ldap != nil } -func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, labels config.AppLabels) bool { +func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, labels config.App) bool { if context.OAuth { log.Debug().Msg("Checking OAuth whitelist") return utils.CheckFilter(labels.OAuth.Whitelist, context.Email) @@ -322,7 +322,7 @@ func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserConte return false } -func (auth *AuthService) IsAuthEnabled(uri string, path config.PathLabels) (bool, error) { +func (auth *AuthService) IsAuthEnabled(uri string, path config.AppPath) (bool, error) { // Check for block list if path.Block != "" { regex, err := regexp.Compile(path.Block) @@ -364,7 +364,7 @@ func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User { } } -func (auth *AuthService) CheckIP(labels config.IPLabels, ip string) bool { +func (auth *AuthService) CheckIP(labels config.AppIP, ip string) bool { for _, blocked := range labels.Block { res, err := utils.FilterIP(blocked, ip) if err != nil { @@ -398,7 +398,7 @@ func (auth *AuthService) CheckIP(labels config.IPLabels, ip string) bool { return true } -func (auth *AuthService) IsBypassedIP(labels config.IPLabels, ip string) bool { +func (auth *AuthService) IsBypassedIP(labels config.AppIP, ip string) bool { for _, bypassed := range labels.Bypass { res, err := utils.FilterIP(bypassed, ip) if err != nil { diff --git a/internal/service/docker_service.go b/internal/service/docker_service.go index f4ce2369..d2a4cfc4 100644 --- a/internal/service/docker_service.go +++ b/internal/service/docker_service.go @@ -4,7 +4,7 @@ import ( "context" "strings" "tinyauth/internal/config" - "tinyauth/internal/utils" + "tinyauth/internal/utils/decoders" container "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" @@ -55,17 +55,17 @@ func (docker *DockerService) DockerConnected() bool { return err == nil } -func (docker *DockerService) GetLabels(appDomain string) (config.AppLabels, error) { +func (docker *DockerService) GetLabels(appDomain string) (config.App, error) { isConnected := docker.DockerConnected() if !isConnected { log.Debug().Msg("Docker not connected, returning empty labels") - return config.AppLabels{}, nil + return config.App{}, nil } containers, err := docker.GetContainers() if err != nil { - return config.AppLabels{}, err + return config.App{}, err } for _, ctr := range containers { @@ -75,7 +75,7 @@ func (docker *DockerService) GetLabels(appDomain string) (config.AppLabels, erro continue } - labels, err := utils.GetLabels(inspect.Config.Labels) + labels, err := decoders.DecodeLabels(inspect.Config.Labels) if err != nil { log.Warn().Str("id", ctr.ID).Err(err).Msg("Error getting container labels, skipping") continue @@ -95,5 +95,5 @@ func (docker *DockerService) GetLabels(appDomain string) (config.AppLabels, erro } log.Debug().Msg("No matching container found, returning empty labels") - return config.AppLabels{}, nil + return config.App{}, nil } diff --git a/internal/utils/decoders/label_decoder.go b/internal/utils/decoders/label_decoder.go new file mode 100644 index 00000000..e83e275e --- /dev/null +++ b/internal/utils/decoders/label_decoder.go @@ -0,0 +1,19 @@ +package decoders + +import ( + "tinyauth/internal/config" + + "github.com/traefik/paerser/parser" +) + +func DecodeLabels(labels map[string]string) (config.Apps, error) { + var appLabels config.Apps + + err := parser.Decode(labels, &appLabels, "tinyauth", "tinyauth.apps") + + if err != nil { + return config.Apps{}, err + } + + return appLabels, nil +} diff --git a/internal/utils/decoders/label_decoder_test.go b/internal/utils/decoders/label_decoder_test.go new file mode 100644 index 00000000..1df885cd --- /dev/null +++ b/internal/utils/decoders/label_decoder_test.go @@ -0,0 +1,73 @@ +package decoders_test + +import ( + "reflect" + "testing" + "tinyauth/internal/config" + "tinyauth/internal/utils/decoders" +) + +func TestDecodeLabels(t *testing.T) { + // Variables + expected := config.Apps{ + Apps: map[string]config.App{ + "foo": { + Config: config.AppConfig{ + Domain: "example.com", + }, + Users: config.AppUsers{ + Allow: "user1,user2", + Block: "user3", + }, + OAuth: config.AppOAuth{ + Whitelist: "somebody@example.com", + Groups: "group3", + }, + IP: config.AppIP{ + Allow: []string{"10.71.0.1/24", "10.71.0.2"}, + Block: []string{"10.10.10.10", "10.0.0.0/24"}, + Bypass: []string{"192.168.1.1"}, + }, + Response: config.AppResponse{ + Headers: []string{"X-Foo=Bar", "X-Baz=Qux"}, + BasicAuth: config.AppBasicAuth{ + Username: "admin", + Password: "password", + PasswordFile: "/path/to/passwordfile", + }, + }, + Path: config.AppPath{ + Allow: "/public", + Block: "/private", + }, + }, + }, + } + test := map[string]string{ + "tinyauth.apps.foo.config.domain": "example.com", + "tinyauth.apps.foo.users.allow": "user1,user2", + "tinyauth.apps.foo.users.block": "user3", + "tinyauth.apps.foo.oauth.whitelist": "somebody@example.com", + "tinyauth.apps.foo.oauth.groups": "group3", + "tinyauth.apps.foo.ip.allow": "10.71.0.1/24,10.71.0.2", + "tinyauth.apps.foo.ip.block": "10.10.10.10,10.0.0.0/24", + "tinyauth.apps.foo.ip.bypass": "192.168.1.1", + "tinyauth.apps.foo.response.headers": "X-Foo=Bar,X-Baz=Qux", + "tinyauth.apps.foo.response.basicauth.username": "admin", + "tinyauth.apps.foo.response.basicauth.password": "password", + "tinyauth.apps.foo.response.basicauth.passwordfile": "/path/to/passwordfile", + "tinyauth.apps.foo.path.allow": "/public", + "tinyauth.apps.foo.path.block": "/private", + } + + // Test + result, err := decoders.DecodeLabels(test) + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if reflect.DeepEqual(expected, result) == false { + t.Fatalf("Expected %v but got %v", expected, result) + } +} diff --git a/internal/utils/label_utils.go b/internal/utils/label_utils.go index 5e423f7a..2ef9a70b 100644 --- a/internal/utils/label_utils.go +++ b/internal/utils/label_utils.go @@ -3,22 +3,8 @@ package utils import ( "net/http" "strings" - "tinyauth/internal/config" - - "github.com/traefik/paerser/parser" ) -func GetLabels(labels map[string]string) (config.Labels, error) { - var labelsParsed config.Labels - - err := parser.Decode(labels, &labelsParsed, "tinyauth", "tinyauth.apps") - if err != nil { - return config.Labels{}, err - } - - return labelsParsed, nil -} - func ParseHeaders(headers []string) map[string]string { headerMap := make(map[string]string) for _, header := range headers { From 7afea8b3fcd3db817c0d9c4de800d7e0b2c84b59 Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 3 Sep 2025 12:45:23 +0300 Subject: [PATCH 016/127] tests: add tests for context controller --- go.mod | 12 +- go.sum | 8 -- .../controller/context_controller_test.go | 116 ++++++++++++++++++ 3 files changed, 121 insertions(+), 15 deletions(-) create mode 100644 internal/controller/context_controller_test.go diff --git a/go.mod b/go.mod index cea16ddd..c4855ffb 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23.2 require ( github.com/cenkalti/backoff/v5 v5.0.3 github.com/gin-gonic/gin v1.10.1 + github.com/glebarez/sqlite v1.11.0 github.com/go-playground/validator/v10 v10.27.0 github.com/golang-migrate/migrate/v4 v4.18.3 github.com/google/go-querystring v1.1.0 @@ -15,9 +16,9 @@ require ( github.com/spf13/viper v1.20.1 github.com/traefik/paerser v0.2.2 golang.org/x/crypto v0.41.0 - gorm.io/driver/sqlite v1.6.0 + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b gorm.io/gorm v1.30.1 - modernc.org/sqlite v1.38.2 + gotest.tools/v3 v3.5.2 ) require ( @@ -28,9 +29,9 @@ require ( github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect - github.com/glebarez/sqlite v1.11.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect @@ -44,12 +45,11 @@ require ( go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect go.opentelemetry.io/otel/sdk v1.34.0 // indirect - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/term v0.34.0 // indirect - gotest.tools/v3 v3.5.2 // indirect modernc.org/libc v1.66.3 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.38.2 // indirect rsc.io/qr v0.2.0 // indirect ) @@ -86,8 +86,6 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/goccy/go-json v0.10.4 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/gorilla/securecookie v1.1.2 // indirect - github.com/gorilla/sessions v1.4.0 github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect diff --git a/go.sum b/go.sum index 0b0aceb6..6e7d9c7c 100644 --- a/go.sum +++ b/go.sum @@ -132,16 +132,10 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= -github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= -github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= -github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -380,8 +374,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= -gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4= gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= diff --git a/internal/controller/context_controller_test.go b/internal/controller/context_controller_test.go new file mode 100644 index 00000000..ecfdf51b --- /dev/null +++ b/internal/controller/context_controller_test.go @@ -0,0 +1,116 @@ +package controller_test + +import ( + "encoding/json" + "net/http/httptest" + "testing" + "tinyauth/internal/config" + "tinyauth/internal/controller" + + "github.com/gin-gonic/gin" + "gotest.tools/v3/assert" +) + +func TestAppContextHandler(t *testing.T) { + // Setup + controllerCfg := controller.ContextControllerConfig{ + ConfiguredProviders: []string{"github", "google", "generic"}, + Title: "Test App", + GenericName: "Generic", + AppURL: "http://localhost:8080", + RootDomain: "localhost", + ForgotPasswordMessage: "Contact admin to reset your password.", + BackgroundImage: "/assets/bg.jpg", + OAuthAutoRedirect: "google", + } + + expectedRes := controller.AppContextResponse{ + Status: 200, + Message: "Success", + ConfiguredProviders: controllerCfg.ConfiguredProviders, + Title: controllerCfg.Title, + GenericName: controllerCfg.GenericName, + AppURL: controllerCfg.AppURL, + RootDomain: controllerCfg.RootDomain, + ForgotPasswordMessage: controllerCfg.ForgotPasswordMessage, + BackgroundImage: controllerCfg.BackgroundImage, + OAuthAutoRedirect: controllerCfg.OAuthAutoRedirect, + } + + gin.SetMode(gin.TestMode) + router := gin.Default() + group := router.Group("/api") + recorder := httptest.NewRecorder() + + // Test + ctrl := controller.NewContextController(controllerCfg, group) + ctrl.SetupRoutes() + + req := httptest.NewRequest("GET", "/api/context/app", nil) + router.ServeHTTP(recorder, req) + + assert.Equal(t, 200, recorder.Code) + + var ctrlRes controller.AppContextResponse + + err := json.Unmarshal(recorder.Body.Bytes(), &ctrlRes) + + assert.NilError(t, err) + assert.DeepEqual(t, expectedRes, ctrlRes) +} + +func TestUserContextController(t *testing.T) { + // Setup + controllerCfg := controller.ContextControllerConfig{} + + userContext := config.UserContext{ + Username: "testuser", + Name: "testuser", + Email: "test@example.com", + IsLoggedIn: true, + OAuth: false, + Provider: "username", + TotpPending: false, + OAuthGroups: "", + TotpEnabled: false, + } + + expectedRes := controller.UserContextResponse{ + Status: 200, + Message: "Success", + IsLoggedIn: userContext.IsLoggedIn, + Username: userContext.Username, + Name: userContext.Name, + Email: userContext.Email, + Provider: userContext.Provider, + OAuth: userContext.OAuth, + TotpPending: userContext.TotpPending, + } + + gin.SetMode(gin.TestMode) + router := gin.Default() + recorder := httptest.NewRecorder() + + router.Use(func(c *gin.Context) { + c.Set("context", &userContext) + c.Next() + }) + + group := router.Group("/api") + + // Test + ctrl := controller.NewContextController(controllerCfg, group) + ctrl.SetupRoutes() + + req := httptest.NewRequest("GET", "/api/context/user", nil) + router.ServeHTTP(recorder, req) + + assert.Equal(t, 200, recorder.Code) + + var ctrlRes controller.UserContextResponse + + err := json.Unmarshal(recorder.Body.Bytes(), &ctrlRes) + + assert.NilError(t, err) + assert.DeepEqual(t, expectedRes, ctrlRes) +} From 9b7dcfd86f1d44e2548d86cda821eb1e01d029b6 Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 3 Sep 2025 13:28:27 +0300 Subject: [PATCH 017/127] tests: add user controller tests --- .../controller/context_controller_test.go | 95 +++--- internal/controller/user_controller_test.go | 283 ++++++++++++++++++ 2 files changed, 327 insertions(+), 51 deletions(-) create mode 100644 internal/controller/user_controller_test.go diff --git a/internal/controller/context_controller_test.go b/internal/controller/context_controller_test.go index ecfdf51b..61d2e8bb 100644 --- a/internal/controller/context_controller_test.go +++ b/internal/controller/context_controller_test.go @@ -11,19 +11,49 @@ import ( "gotest.tools/v3/assert" ) -func TestAppContextHandler(t *testing.T) { +var controllerCfg = controller.ContextControllerConfig{ + ConfiguredProviders: []string{"github", "google", "generic"}, + Title: "Test App", + GenericName: "Generic", + AppURL: "http://localhost:8080", + RootDomain: "localhost", + ForgotPasswordMessage: "Contact admin to reset your password.", + BackgroundImage: "/assets/bg.jpg", + OAuthAutoRedirect: "google", +} + +var userContext = config.UserContext{ + Username: "testuser", + Name: "testuser", + Email: "test@example.com", + IsLoggedIn: true, + OAuth: false, + Provider: "username", + TotpPending: false, + OAuthGroups: "", + TotpEnabled: false, +} + +func setupContextController() (*gin.Engine, *httptest.ResponseRecorder) { // Setup - controllerCfg := controller.ContextControllerConfig{ - ConfiguredProviders: []string{"github", "google", "generic"}, - Title: "Test App", - GenericName: "Generic", - AppURL: "http://localhost:8080", - RootDomain: "localhost", - ForgotPasswordMessage: "Contact admin to reset your password.", - BackgroundImage: "/assets/bg.jpg", - OAuthAutoRedirect: "google", - } + gin.SetMode(gin.TestMode) + router := gin.Default() + recorder := httptest.NewRecorder() + + router.Use(func(c *gin.Context) { + c.Set("context", &userContext) + c.Next() + }) + + group := router.Group("/api") + + ctrl := controller.NewContextController(controllerCfg, group) + ctrl.SetupRoutes() + + return router, recorder +} +func TestAppContextHandler(t *testing.T) { expectedRes := controller.AppContextResponse{ Status: 200, Message: "Success", @@ -37,15 +67,7 @@ func TestAppContextHandler(t *testing.T) { OAuthAutoRedirect: controllerCfg.OAuthAutoRedirect, } - gin.SetMode(gin.TestMode) - router := gin.Default() - group := router.Group("/api") - recorder := httptest.NewRecorder() - - // Test - ctrl := controller.NewContextController(controllerCfg, group) - ctrl.SetupRoutes() - + router, recorder := setupContextController() req := httptest.NewRequest("GET", "/api/context/app", nil) router.ServeHTTP(recorder, req) @@ -59,22 +81,7 @@ func TestAppContextHandler(t *testing.T) { assert.DeepEqual(t, expectedRes, ctrlRes) } -func TestUserContextController(t *testing.T) { - // Setup - controllerCfg := controller.ContextControllerConfig{} - - userContext := config.UserContext{ - Username: "testuser", - Name: "testuser", - Email: "test@example.com", - IsLoggedIn: true, - OAuth: false, - Provider: "username", - TotpPending: false, - OAuthGroups: "", - TotpEnabled: false, - } - +func TestUserContextHandler(t *testing.T) { expectedRes := controller.UserContextResponse{ Status: 200, Message: "Success", @@ -87,21 +94,7 @@ func TestUserContextController(t *testing.T) { TotpPending: userContext.TotpPending, } - gin.SetMode(gin.TestMode) - router := gin.Default() - recorder := httptest.NewRecorder() - - router.Use(func(c *gin.Context) { - c.Set("context", &userContext) - c.Next() - }) - - group := router.Group("/api") - - // Test - ctrl := controller.NewContextController(controllerCfg, group) - ctrl.SetupRoutes() - + router, recorder := setupContextController() req := httptest.NewRequest("GET", "/api/context/user", nil) router.ServeHTTP(recorder, req) diff --git a/internal/controller/user_controller_test.go b/internal/controller/user_controller_test.go new file mode 100644 index 00000000..c22a949f --- /dev/null +++ b/internal/controller/user_controller_test.go @@ -0,0 +1,283 @@ +package controller_test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + "tinyauth/internal/config" + "tinyauth/internal/controller" + "tinyauth/internal/service" + + "github.com/gin-gonic/gin" + "github.com/pquerna/otp/totp" + "gotest.tools/v3/assert" +) + +var cookieValue string +var totpSecret = "6WFZXPEZRK5MZHHYAFW4DAOUYQMCASBJ" + +func setupUserController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) { + // Setup + gin.SetMode(gin.TestMode) + router := gin.Default() + + if middlewares != nil { + for _, m := range *middlewares { + router.Use(m) + } + } + + group := router.Group("/api") + recorder := httptest.NewRecorder() + + // Database + databaseService := service.NewDatabaseService(service.DatabaseServiceConfig{ + DatabasePath: "/tmp/tinyauth_test.db", + }) + + assert.NilError(t, databaseService.Init()) + + database := databaseService.GetDatabase() + + // Auth service + authService := service.NewAuthService(service.AuthServiceConfig{ + Users: []config.User{ + { + Username: "testuser", + Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test + }, + { + Username: "totpuser", + Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test + TotpSecret: totpSecret, + }, + }, + OauthWhitelist: "", + SessionExpiry: 3600, + SecureCookie: false, + RootDomain: "localhost", + LoginTimeout: 300, + LoginMaxRetries: 3, + SessionCookieName: "tinyauth-session", + }, nil, nil, database) + + // Controller + ctrl := controller.NewUserController(controller.UserControllerConfig{ + RootDomain: "localhost", + }, group, authService) + ctrl.SetupRoutes() + + return router, recorder +} + +func TestLoginHandler(t *testing.T) { + // Setup + router, recorder := setupUserController(t, nil) + + loginReq := controller.LoginRequest{ + Username: "testuser", + Password: "test", + } + + loginReqJson, err := json.Marshal(loginReq) + assert.NilError(t, err) + + // Test + req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqJson))) + router.ServeHTTP(recorder, req) + + assert.Equal(t, 200, recorder.Code) + + cookie := recorder.Result().Cookies()[0] + + assert.Equal(t, "tinyauth-session", cookie.Name) + assert.Assert(t, cookie.Value != "") + + cookieValue = cookie.Value + + // Test invalid credentials + loginReq = controller.LoginRequest{ + Username: "testuser", + Password: "invalid", + } + + loginReqJson, err = json.Marshal(loginReq) + assert.NilError(t, err) + + recorder = httptest.NewRecorder() + req = httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqJson))) + router.ServeHTTP(recorder, req) + + assert.Equal(t, 401, recorder.Code) + + // Test totp required + loginReq = controller.LoginRequest{ + Username: "totpuser", + Password: "test", + } + + loginReqJson, err = json.Marshal(loginReq) + assert.NilError(t, err) + + recorder = httptest.NewRecorder() + req = httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqJson))) + router.ServeHTTP(recorder, req) + + assert.Equal(t, 200, recorder.Code) + + loginResJson, err := json.Marshal(map[string]any{ + "message": "TOTP required", + "status": 200, + "totpPending": true, + }) + + assert.NilError(t, err) + assert.Equal(t, string(loginResJson), recorder.Body.String()) + + // Test rate limiting + loginReq = controller.LoginRequest{ + Username: "testuser", + Password: "invalid", + } + + loginReqJson, err = json.Marshal(loginReq) + assert.NilError(t, err) + + for range 5 { + recorder = httptest.NewRecorder() + req = httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqJson))) + router.ServeHTTP(recorder, req) + } + + assert.Equal(t, 429, recorder.Code) +} + +func TestLogoutHandler(t *testing.T) { + // Setup + router, recorder := setupUserController(t, nil) + + // Test + req := httptest.NewRequest("POST", "/api/user/logout", nil) + + req.AddCookie(&http.Cookie{ + Name: "tinyauth-session", + Value: cookieValue, + }) + + router.ServeHTTP(recorder, req) + + assert.Equal(t, 200, recorder.Code) + + cookie := recorder.Result().Cookies()[0] + + assert.Equal(t, "tinyauth-session", cookie.Name) + assert.Equal(t, "", cookie.Value) + assert.Equal(t, -1, cookie.MaxAge) +} + +func TestTotpHandler(t *testing.T) { + // Setup + router, recorder := setupUserController(t, &[]gin.HandlerFunc{ + func(c *gin.Context) { + c.Set("context", &config.UserContext{ + Username: "totpuser", + Name: "totpuser", + Email: "totpuser@example.com", + IsLoggedIn: false, + OAuth: false, + Provider: "username", + TotpPending: true, + OAuthGroups: "", + TotpEnabled: true, + }) + c.Next() + }, + }) + + // Test + code, err := totp.GenerateCode(totpSecret, time.Now()) + + assert.NilError(t, err) + + totpReq := controller.TotpRequest{ + Code: code, + } + + totpReqJson, err := json.Marshal(totpReq) + assert.NilError(t, err) + + req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqJson))) + router.ServeHTTP(recorder, req) + + assert.Equal(t, 200, recorder.Code) + + cookie := recorder.Result().Cookies()[0] + + assert.Equal(t, "tinyauth-session", cookie.Name) + assert.Assert(t, cookie.Value != "") + + // Test rate limiting + totpReq = controller.TotpRequest{ + Code: "000000", + } + + totpReqJson, err = json.Marshal(totpReq) + assert.NilError(t, err) + + for range 5 { + recorder = httptest.NewRecorder() + req = httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqJson))) + router.ServeHTTP(recorder, req) + } + + assert.Equal(t, 429, recorder.Code) + + // Test invalid code + router, recorder = setupUserController(t, &[]gin.HandlerFunc{ + func(c *gin.Context) { + c.Set("context", &config.UserContext{ + Username: "totpuser", + Name: "totpuser", + Email: "totpuser@example.com", + IsLoggedIn: false, + OAuth: false, + Provider: "username", + TotpPending: true, + OAuthGroups: "", + TotpEnabled: true, + }) + c.Next() + }, + }) + + req = httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqJson))) + router.ServeHTTP(recorder, req) + + assert.Equal(t, 401, recorder.Code) + + // Test no totp pending + router, recorder = setupUserController(t, &[]gin.HandlerFunc{ + func(c *gin.Context) { + c.Set("context", &config.UserContext{ + Username: "totpuser", + Name: "totpuser", + Email: "totpuser@example.com", + IsLoggedIn: false, + OAuth: false, + Provider: "username", + TotpPending: false, + OAuthGroups: "", + TotpEnabled: false, + }) + c.Next() + }, + }) + + req = httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqJson))) + router.ServeHTTP(recorder, req) + + assert.Equal(t, 401, recorder.Code) +} From 53856e0a70c8edfa0252384b36383f938780c1eb Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 3 Sep 2025 13:31:45 +0300 Subject: [PATCH 018/127] tests: test invalid json in user controller --- internal/controller/user_controller_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/internal/controller/user_controller_test.go b/internal/controller/user_controller_test.go index c22a949f..93982bcc 100644 --- a/internal/controller/user_controller_test.go +++ b/internal/controller/user_controller_test.go @@ -137,6 +137,13 @@ func TestLoginHandler(t *testing.T) { assert.NilError(t, err) assert.Equal(t, string(loginResJson), recorder.Body.String()) + // Test invalid json + recorder = httptest.NewRecorder() + req = httptest.NewRequest("POST", "/api/user/login", strings.NewReader("{invalid json}")) + router.ServeHTTP(recorder, req) + + assert.Equal(t, 400, recorder.Code) + // Test rate limiting loginReq = controller.LoginRequest{ Username: "testuser", @@ -219,6 +226,13 @@ func TestTotpHandler(t *testing.T) { assert.Equal(t, "tinyauth-session", cookie.Name) assert.Assert(t, cookie.Value != "") + // Test invalid json + recorder = httptest.NewRecorder() + req = httptest.NewRequest("POST", "/api/user/totp", strings.NewReader("{invalid json}")) + router.ServeHTTP(recorder, req) + + assert.Equal(t, 400, recorder.Code) + // Test rate limiting totpReq = controller.TotpRequest{ Code: "000000", From f8836fc96464ad3c54162d5e8c068f3f7192ce67 Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 3 Sep 2025 13:36:11 +0300 Subject: [PATCH 019/127] tests: test user context handler with no context --- .../controller/context_controller_test.go | 40 +++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/internal/controller/context_controller_test.go b/internal/controller/context_controller_test.go index 61d2e8bb..af64b218 100644 --- a/internal/controller/context_controller_test.go +++ b/internal/controller/context_controller_test.go @@ -34,16 +34,17 @@ var userContext = config.UserContext{ TotpEnabled: false, } -func setupContextController() (*gin.Engine, *httptest.ResponseRecorder) { +func setupContextController(middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) { // Setup gin.SetMode(gin.TestMode) router := gin.Default() recorder := httptest.NewRecorder() - router.Use(func(c *gin.Context) { - c.Set("context", &userContext) - c.Next() - }) + if middlewares != nil { + for _, m := range *middlewares { + router.Use(m) + } + } group := router.Group("/api") @@ -67,7 +68,7 @@ func TestAppContextHandler(t *testing.T) { OAuthAutoRedirect: controllerCfg.OAuthAutoRedirect, } - router, recorder := setupContextController() + router, recorder := setupContextController(nil) req := httptest.NewRequest("GET", "/api/context/app", nil) router.ServeHTTP(recorder, req) @@ -94,7 +95,14 @@ func TestUserContextHandler(t *testing.T) { TotpPending: userContext.TotpPending, } - router, recorder := setupContextController() + // Test with context + router, recorder := setupContextController(&[]gin.HandlerFunc{ + func(c *gin.Context) { + c.Set("context", &userContext) + c.Next() + }, + }) + req := httptest.NewRequest("GET", "/api/context/user", nil) router.ServeHTTP(recorder, req) @@ -106,4 +114,22 @@ func TestUserContextHandler(t *testing.T) { assert.NilError(t, err) assert.DeepEqual(t, expectedRes, ctrlRes) + + // Test no context + expectedRes = controller.UserContextResponse{ + Status: 401, + Message: "Unauthorized", + IsLoggedIn: false, + } + + router, recorder = setupContextController(nil) + req = httptest.NewRequest("GET", "/api/context/user", nil) + router.ServeHTTP(recorder, req) + + assert.Equal(t, 200, recorder.Code) + + err = json.Unmarshal(recorder.Body.Bytes(), &ctrlRes) + + assert.NilError(t, err) + assert.DeepEqual(t, expectedRes, ctrlRes) } From bb0373758a713d8d8658a97477263f335bc11da3 Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 3 Sep 2025 14:58:24 +0300 Subject: [PATCH 020/127] tests: add resources controller test --- .../controller/resources_controller_test.go | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 internal/controller/resources_controller_test.go diff --git a/internal/controller/resources_controller_test.go b/internal/controller/resources_controller_test.go new file mode 100644 index 00000000..732b2f70 --- /dev/null +++ b/internal/controller/resources_controller_test.go @@ -0,0 +1,56 @@ +package controller_test + +import ( + "net/http/httptest" + "os" + "testing" + "tinyauth/internal/controller" + + "github.com/gin-gonic/gin" + "gotest.tools/v3/assert" +) + +func TestResourcesHandler(t *testing.T) { + // Setup + gin.SetMode(gin.TestMode) + router := gin.New() + group := router.Group("/") + + ctrl := controller.NewResourcesController(controller.ResourcesControllerConfig{ + ResourcesDir: "/tmp/tinyauth", + }, group) + ctrl.SetupRoutes() + + // Create test data + err := os.Mkdir("/tmp/tinyauth", 0755) + assert.NilError(t, err) + + file, err := os.Create("/tmp/tinyauth/test.txt") + assert.NilError(t, err) + + _, err = file.WriteString("This is a test file.") + assert.NilError(t, err) + file.Close() + + // Test existing file + req := httptest.NewRequest("GET", "/resources/test.txt", nil) + recorder := httptest.NewRecorder() + router.ServeHTTP(recorder, req) + + assert.Equal(t, 200, recorder.Code) + assert.Equal(t, "This is a test file.", recorder.Body.String()) + + // Test non-existing file + req = httptest.NewRequest("GET", "/resources/nonexistent.txt", nil) + recorder = httptest.NewRecorder() + router.ServeHTTP(recorder, req) + + assert.Equal(t, 404, recorder.Code) + + // Test directory traversal attack + req = httptest.NewRequest("GET", "/resources/../etc/passwd", nil) + recorder = httptest.NewRecorder() + router.ServeHTTP(recorder, req) + + assert.Equal(t, 404, recorder.Code) +} From ba46493a7b37e2e312d944e04555e82d6acb4185 Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 3 Sep 2025 15:30:24 +0300 Subject: [PATCH 021/127] tests: add proxy controller tests --- internal/controller/proxy_controller.go | 9 + internal/controller/proxy_controller_test.go | 164 +++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 internal/controller/proxy_controller_test.go diff --git a/internal/controller/proxy_controller.go b/internal/controller/proxy_controller.go index 88eeb4dc..dbf13b92 100644 --- a/internal/controller/proxy_controller.go +++ b/internal/controller/proxy_controller.go @@ -55,6 +55,15 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { return } + if req.Proxy != "nginx" && req.Proxy != "traefik" && req.Proxy != "caddy" { + log.Warn().Str("proxy", req.Proxy).Msg("Invalid proxy") + c.JSON(400, gin.H{ + "status": 400, + "message": "Bad Request", + }) + return + } + isBrowser := strings.Contains(c.Request.Header.Get("Accept"), "text/html") if isBrowser { diff --git a/internal/controller/proxy_controller_test.go b/internal/controller/proxy_controller_test.go new file mode 100644 index 00000000..718eb0d4 --- /dev/null +++ b/internal/controller/proxy_controller_test.go @@ -0,0 +1,164 @@ +package controller_test + +import ( + "net/http/httptest" + "testing" + "tinyauth/internal/config" + "tinyauth/internal/controller" + "tinyauth/internal/service" + + "github.com/gin-gonic/gin" + "gotest.tools/v3/assert" +) + +func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder, *service.AuthService) { + // Setup + gin.SetMode(gin.TestMode) + router := gin.Default() + + if middlewares != nil { + for _, m := range *middlewares { + router.Use(m) + } + } + + group := router.Group("/api") + recorder := httptest.NewRecorder() + + // Database + databaseService := service.NewDatabaseService(service.DatabaseServiceConfig{ + DatabasePath: "/tmp/tinyauth_test.db", + }) + + assert.NilError(t, databaseService.Init()) + + database := databaseService.GetDatabase() + + // Docker + dockerService := service.NewDockerService() + + assert.NilError(t, dockerService.Init()) + + // Auth service + authService := service.NewAuthService(service.AuthServiceConfig{ + Users: []config.User{ + { + Username: "testuser", + Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test + }, + }, + OauthWhitelist: "", + SessionExpiry: 3600, + SecureCookie: false, + RootDomain: "localhost", + LoginTimeout: 300, + LoginMaxRetries: 3, + SessionCookieName: "tinyauth-session", + }, dockerService, nil, database) + + // Controller + ctrl := controller.NewProxyController(controller.ProxyControllerConfig{ + AppURL: "http://localhost:8080", + }, group, dockerService, authService) + ctrl.SetupRoutes() + + return router, recorder, authService +} + +func TestProxyHandler(t *testing.T) { + // Setup + router, recorder, authService := setupProxyController(t, nil) + + // Test invalid proxy + req := httptest.NewRequest("GET", "/api/auth/invalidproxy", nil) + router.ServeHTTP(recorder, req) + + assert.Equal(t, 400, recorder.Code) + + // Test logged out user (traefik/caddy) + recorder = httptest.NewRecorder() + req = httptest.NewRequest("GET", "/api/auth/traefik", nil) + req.Header.Set("X-Forwarded-Proto", "https") + req.Header.Set("X-Forwarded-Host", "example.com") + req.Header.Set("X-Forwarded-Uri", "/somepath") + req.Header.Set("Accept", "text/html") + router.ServeHTTP(recorder, req) + + assert.Equal(t, 307, recorder.Code) + assert.Equal(t, "http://localhost:8080/login?redirect_uri=https%3A%2F%2Fexample.com%2Fsomepath", recorder.Header().Get("Location")) + + // Test logged out user (nginx) + recorder = httptest.NewRecorder() + req = httptest.NewRequest("GET", "/api/auth/nginx", nil) + router.ServeHTTP(recorder, req) + + assert.Equal(t, 401, recorder.Code) + + // Test logged in user + c := gin.CreateTestContextOnly(recorder, router) + + err := authService.CreateSessionCookie(c, &config.SessionCookie{ + Username: "testuser", + Name: "testuser", + Email: "testuser@example.com", + Provider: "username", + TotpPending: false, + OAuthGroups: "", + }) + + assert.NilError(t, err) + + cookie := c.Writer.Header().Get("Set-Cookie") + + router, recorder, _ = setupProxyController(t, &[]gin.HandlerFunc{ + func(c *gin.Context) { + c.Set("context", &config.UserContext{ + Username: "testuser", + Name: "testuser", + Email: "testuser@example.com", + IsLoggedIn: true, + OAuth: false, + Provider: "username", + TotpPending: false, + OAuthGroups: "", + TotpEnabled: false, + }) + c.Next() + }, + }) + + req = httptest.NewRequest("GET", "/api/auth/traefik", nil) + req.Header.Set("Cookie", cookie) + req.Header.Set("Accept", "text/html") + router.ServeHTTP(recorder, req) + + assert.Equal(t, 200, recorder.Code) + + assert.Equal(t, "testuser", recorder.Header().Get("Remote-User")) + assert.Equal(t, "testuser", recorder.Header().Get("Remote-Name")) + assert.Equal(t, "testuser@example.com", recorder.Header().Get("Remote-Email")) + + // Ensure basic auth is disabled for TOTP enabled users + router, recorder, _ = setupProxyController(t, &[]gin.HandlerFunc{ + func(c *gin.Context) { + c.Set("context", &config.UserContext{ + Username: "testuser", + Name: "testuser", + Email: "testuser@example.com", + IsLoggedIn: true, + OAuth: false, + Provider: "basic", + TotpPending: false, + OAuthGroups: "", + TotpEnabled: true, + }) + c.Next() + }, + }) + + req = httptest.NewRequest("GET", "/api/auth/traefik", nil) + req.SetBasicAuth("testuser", "test") + router.ServeHTTP(recorder, req) + + assert.Equal(t, 401, recorder.Code) +} From 74cb8067a8fcc672b24e4e587e7ee55e30be7e8e Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 3 Sep 2025 17:52:51 +0300 Subject: [PATCH 022/127] tests: add util tests --- internal/utils/app_utils.go | 9 +- internal/utils/app_utils_test.go | 197 ++++++++++++++++++++++++++ internal/utils/fs_utils_test.go | 31 ++++ internal/utils/label_utils_test.go | 87 ++++++++++++ internal/utils/security_utils.go | 8 +- internal/utils/security_utils_test.go | 151 ++++++++++++++++++++ internal/utils/string_utils_test.go | 50 +++++++ internal/utils/user_utils_test.go | 163 +++++++++++++++++++++ 8 files changed, 690 insertions(+), 6 deletions(-) create mode 100644 internal/utils/app_utils_test.go create mode 100644 internal/utils/fs_utils_test.go create mode 100644 internal/utils/label_utils_test.go create mode 100644 internal/utils/security_utils_test.go create mode 100644 internal/utils/string_utils_test.go create mode 100644 internal/utils/user_utils_test.go diff --git a/internal/utils/app_utils.go b/internal/utils/app_utils.go index 62b95922..57e40f4e 100644 --- a/internal/utils/app_utils.go +++ b/internal/utils/app_utils.go @@ -13,13 +13,13 @@ import ( ) // Get root domain parses a hostname and returns the upper domain (e.g. sub1.sub2.domain.com -> sub2.domain.com) -func GetRootDomain(appUrl string) (string, error) { - appUrlParsed, err := url.Parse(appUrl) +func GetRootDomain(u string) (string, error) { + appUrl, err := url.Parse(u) if err != nil { return "", err } - host := appUrlParsed.Hostname() + host := appUrl.Hostname() if netIP := net.ParseIP(host); netIP != nil { return "", errors.New("IP addresses are not allowed") @@ -27,7 +27,7 @@ func GetRootDomain(appUrl string) (string, error) { urlParts := strings.Split(host, ".") - if len(urlParts) < 2 { + if len(urlParts) < 3 { return "", errors.New("invalid domain, must be at least second level domain") } @@ -49,6 +49,7 @@ func ParseFileToLine(content string) string { } func Filter[T any](slice []T, test func(T) bool) (res []T) { + res = make([]T, 0) for _, value := range slice { if test(value) { res = append(res, value) diff --git a/internal/utils/app_utils_test.go b/internal/utils/app_utils_test.go new file mode 100644 index 00000000..1540c76c --- /dev/null +++ b/internal/utils/app_utils_test.go @@ -0,0 +1,197 @@ +package utils_test + +import ( + "testing" + "tinyauth/internal/config" + "tinyauth/internal/utils" + + "github.com/gin-gonic/gin" + "gotest.tools/v3/assert" +) + +func TestGetRootDomain(t *testing.T) { + // Normal case + domain := "http://sub.example.com" + expected := "example.com" + result, err := utils.GetRootDomain(domain) + assert.NilError(t, err) + assert.Equal(t, expected, result) + + // Domain with multiple subdomains + domain = "http://b.c.example.com" + expected = "c.example.com" + result, err = utils.GetRootDomain(domain) + assert.NilError(t, err) + assert.Equal(t, expected, result) + + // Domain with no subdomain + domain = "http://example.com" + expected = "example.com" + _, err = utils.GetRootDomain(domain) + assert.Error(t, err, "invalid domain, must be at least second level domain") + + // Invalid domain (only TLD) + domain = "com" + _, err = utils.GetRootDomain(domain) + assert.ErrorContains(t, err, "invalid domain") + + // IP address + domain = "http://10.10.10.10" + _, err = utils.GetRootDomain(domain) + assert.ErrorContains(t, err, "IP addresses are not allowed") + + // Invalid URL + domain = "http://[::1]:namedport" + _, err = utils.GetRootDomain(domain) + assert.ErrorContains(t, err, "parse \"http://[::1]:namedport\": invalid port \":namedport\" after host") + + // URL with scheme and path + domain = "https://sub.example.com/path" + expected = "example.com" + result, err = utils.GetRootDomain(domain) + assert.NilError(t, err) + assert.Equal(t, expected, result) + + // URL with port + domain = "http://sub.example.com:8080" + expected = "example.com" + result, err = utils.GetRootDomain(domain) + assert.NilError(t, err) + assert.Equal(t, expected, result) +} + +func TestParseFileToLine(t *testing.T) { + // Normal case + content := "user1\nuser2\nuser3" + expected := "user1,user2,user3" + result := utils.ParseFileToLine(content) + assert.Equal(t, expected, result) + + // Case with empty lines and spaces + content = " user1 \n\n user2 \n user3 \n" + expected = "user1,user2,user3" + result = utils.ParseFileToLine(content) + assert.Equal(t, expected, result) + + // Case with only empty lines + content = "\n\n\n" + expected = "" + result = utils.ParseFileToLine(content) + assert.Equal(t, expected, result) + + // Case with single user + content = "singleuser" + expected = "singleuser" + result = utils.ParseFileToLine(content) + assert.Equal(t, expected, result) + + // Case with trailing newline + content = "user1\nuser2\n" + expected = "user1,user2" + result = utils.ParseFileToLine(content) + assert.Equal(t, expected, result) +} + +func TestFilter(t *testing.T) { + // Normal case + slice := []int{1, 2, 3, 4, 5} + testFunc := func(n int) bool { return n%2 == 0 } + expected := []int{2, 4} + result := utils.Filter(slice, testFunc) + assert.DeepEqual(t, expected, result) + + // Case with no matches + slice = []int{1, 3, 5} + testFunc = func(n int) bool { return n%2 == 0 } + expected = []int{} + result = utils.Filter(slice, testFunc) + assert.DeepEqual(t, expected, result) + + // Case with all matches + slice = []int{2, 4, 6} + testFunc = func(n int) bool { return n%2 == 0 } + expected = []int{2, 4, 6} + result = utils.Filter(slice, testFunc) + assert.DeepEqual(t, expected, result) + + // Case with empty slice + slice = []int{} + testFunc = func(n int) bool { return n%2 == 0 } + expected = []int{} + result = utils.Filter(slice, testFunc) + assert.DeepEqual(t, expected, result) + + // Case with different type (string) + sliceStr := []string{"apple", "banana", "cherry"} + testFuncStr := func(s string) bool { return len(s) > 5 } + expectedStr := []string{"banana", "cherry"} + resultStr := utils.Filter(sliceStr, testFuncStr) + assert.DeepEqual(t, expectedStr, resultStr) +} + +func TestGetContext(t *testing.T) { + // Setup + gin.SetMode(gin.TestMode) + c, _ := gin.CreateTestContext(nil) + + // Normal case + c.Set("context", &config.UserContext{Username: "testuser"}) + result, err := utils.GetContext(c) + assert.NilError(t, err) + assert.Equal(t, "testuser", result.Username) + + // Case with no context + c.Set("context", nil) + _, err = utils.GetContext(c) + assert.Error(t, err, "invalid user context in request") + + // Case with invalid context type + c.Set("context", "invalid type") + _, err = utils.GetContext(c) + assert.Error(t, err, "invalid user context in request") +} + +func TestIsRedirectSafe(t *testing.T) { + // Setup + domain := "example.com" + + // Case with no subdomain + redirectURL := "http://example.com/welcome" + result := utils.IsRedirectSafe(redirectURL, domain) + assert.Equal(t, false, result) + + // Case with different domain + redirectURL = "http://malicious.com/phishing" + result = utils.IsRedirectSafe(redirectURL, domain) + assert.Equal(t, false, result) + + // Case with subdomain + redirectURL = "http://sub.example.com/page" + result = utils.IsRedirectSafe(redirectURL, domain) + assert.Equal(t, true, result) + + // Case with empty redirect URL + redirectURL = "" + result = utils.IsRedirectSafe(redirectURL, domain) + assert.Equal(t, false, result) + + // Case with invalid URL + redirectURL = "http://[::1]:namedport" + result = utils.IsRedirectSafe(redirectURL, domain) + assert.Equal(t, false, result) + + // Case with URL having port + redirectURL = "http://sub.example.com:8080/page" + result = utils.IsRedirectSafe(redirectURL, domain) + assert.Equal(t, true, result) + + // Case with URL having different subdomain + redirectURL = "http://another.example.com/page" + result = utils.IsRedirectSafe(redirectURL, domain) + assert.Equal(t, true, result) + + // Case with URL having different TLD + redirectURL = "http://example.org/page" + result = utils.IsRedirectSafe(redirectURL, domain) + assert.Equal(t, false, result) +} diff --git a/internal/utils/fs_utils_test.go b/internal/utils/fs_utils_test.go new file mode 100644 index 00000000..54033ba5 --- /dev/null +++ b/internal/utils/fs_utils_test.go @@ -0,0 +1,31 @@ +package utils + +import ( + "os" + "testing" + + "gotest.tools/v3/assert" +) + +func TestReadFile(t *testing.T) { + // Setup + file, err := os.Create("/tmp/tinyauth_test_file") + assert.NilError(t, err) + + _, err = file.WriteString("file content\n") + assert.NilError(t, err) + + err = file.Close() + assert.NilError(t, err) + defer os.Remove("/tmp/tinyauth_test_file") + + // Normal case + content, err := ReadFile("/tmp/tinyauth_test_file") + assert.NilError(t, err) + assert.Equal(t, "file content\n", content) + + // Non-existing file + content, err = ReadFile("/tmp/non_existing_file") + assert.ErrorContains(t, err, "no such file or directory") + assert.Equal(t, "", content) +} diff --git a/internal/utils/label_utils_test.go b/internal/utils/label_utils_test.go new file mode 100644 index 00000000..f38302d1 --- /dev/null +++ b/internal/utils/label_utils_test.go @@ -0,0 +1,87 @@ +package utils_test + +import ( + "testing" + "tinyauth/internal/utils" + + "gotest.tools/v3/assert" +) + +func TestParseHeaders(t *testing.T) { + // Normal case + headers := []string{ + "X-Custom-Header=Value", + "Another-Header=AnotherValue", + } + expected := map[string]string{ + "X-Custom-Header": "Value", + "Another-Header": "AnotherValue", + } + assert.DeepEqual(t, expected, utils.ParseHeaders(headers)) + + // Case insensitivity and trimming + headers = []string{ + " x-custom-header = Value ", + "ANOTHER-HEADER=AnotherValue", + } + expected = map[string]string{ + "X-Custom-Header": "Value", + "Another-Header": "AnotherValue", + } + assert.DeepEqual(t, expected, utils.ParseHeaders(headers)) + + // Invalid headers (missing '=', empty key/value) + headers = []string{ + "InvalidHeader", + "=NoKey", + "NoValue=", + " = ", + } + expected = map[string]string{} + assert.DeepEqual(t, expected, utils.ParseHeaders(headers)) + + // Headers with unsafe characters + headers = []string{ + "X-Custom-Header=Val\x00ue", // Null byte + "Another-Header=Anoth\x7FerValue", // DEL character + "Good-Header=GoodValue", + } + expected = map[string]string{ + "X-Custom-Header": "Value", + "Another-Header": "AnotherValue", + "Good-Header": "GoodValue", + } + assert.DeepEqual(t, expected, utils.ParseHeaders(headers)) + + // Header with spaces in key (should be ignored) + headers = []string{ + "X Custom Header=Value", + "Valid-Header=ValidValue", + } + expected = map[string]string{ + "Valid-Header": "ValidValue", + } + assert.DeepEqual(t, expected, utils.ParseHeaders(headers)) +} + +func TestSanitizeHeader(t *testing.T) { + // Normal case + header := "X-Custom-Header" + expected := "X-Custom-Header" + assert.Equal(t, expected, utils.SanitizeHeader(header)) + + // Header with unsafe characters + header = "X-Cust\x00om-Hea\x7Fder" // Null byte and DEL character + expected = "X-Custom-Header" + assert.Equal(t, expected, utils.SanitizeHeader(header)) + + // Header with only unsafe characters + header = "\x00\x01\x02\x7F" + expected = "" + assert.Equal(t, expected, utils.SanitizeHeader(header)) + + // Header with spaces and tabs (should be preserved) + header = "X Custom\tHeader" + expected = "X Custom\tHeader" + assert.Equal(t, expected, utils.SanitizeHeader(header)) +} diff --git a/internal/utils/security_utils.go b/internal/utils/security_utils.go index 85a359db..91e17ee1 100644 --- a/internal/utils/security_utils.go +++ b/internal/utils/security_utils.go @@ -48,6 +48,12 @@ func GetBasicAuth(username string, password string) string { func FilterIP(filter string, ip string) (bool, error) { ipAddr := net.ParseIP(ip) + if ipAddr == nil { + return false, errors.New("invalid IP address") + } + + filter = strings.Replace(filter, "-", "/", -1) + if strings.Contains(filter, "/") { _, cidr, err := net.ParseCIDR(filter) if err != nil { @@ -73,8 +79,6 @@ func CheckFilter(filter string, str string) bool { return true } - filter = strings.Replace(filter, "-", "/", -1) - if strings.HasPrefix(filter, "/") && strings.HasSuffix(filter, "/") { re, err := regexp.Compile(filter[1 : len(filter)-1]) if err != nil { diff --git a/internal/utils/security_utils_test.go b/internal/utils/security_utils_test.go new file mode 100644 index 00000000..941f853f --- /dev/null +++ b/internal/utils/security_utils_test.go @@ -0,0 +1,151 @@ +package utils_test + +import ( + "os" + "testing" + "tinyauth/internal/utils" + + "gotest.tools/v3/assert" +) + +func TestGetSecret(t *testing.T) { + // Setup + file, err := os.Create("/tmp/tinyauth_test_secret") + assert.NilError(t, err) + + _, err = file.WriteString(" secret \n") + assert.NilError(t, err) + + err = file.Close() + assert.NilError(t, err) + defer os.Remove("/tmp/tinyauth_test_secret") + + // Get from config + assert.Equal(t, "mysecret", utils.GetSecret("mysecret", "")) + + // Get from file + assert.Equal(t, "secret", utils.GetSecret("", "/tmp/tinyauth_test_secret")) + + // Get from both (config should take precedence) + assert.Equal(t, "mysecret", utils.GetSecret("mysecret", "/tmp/tinyauth_test_secret")) + + // Get from none + assert.Equal(t, "", utils.GetSecret("", "")) + + // Get from non-existing file + assert.Equal(t, "", utils.GetSecret("", "/tmp/non_existing_file")) +} + +func TestParseSecretFile(t *testing.T) { + // Normal case + content := " mysecret \n" + assert.Equal(t, "mysecret", utils.ParseSecretFile(content)) + + // Multiple lines (should take the first non-empty line) + content = "\n\n firstsecret \nsecondsecret\n" + assert.Equal(t, "firstsecret", utils.ParseSecretFile(content)) + + // All empty lines + content = "\n \n \n" + assert.Equal(t, "", utils.ParseSecretFile(content)) + + // Empty content + content = "" + assert.Equal(t, "", utils.ParseSecretFile(content)) +} + +func TestGetBasicAuth(t *testing.T) { + // Normal case + username := "user" + password := "pass" + expected := "dXNlcjpwYXNz" // base64 of "user:pass" + assert.Equal(t, expected, utils.GetBasicAuth(username, password)) + + // Empty username + username = "" + password = "pass" + expected = "OnBhc3M=" // base64 of ":pass" + assert.Equal(t, expected, utils.GetBasicAuth(username, password)) + + // Empty password + username = "user" + password = "" + expected = "dXNlcjo=" // base64 of "user:" + assert.Equal(t, expected, utils.GetBasicAuth(username, password)) +} + +func TestFilterIP(t *testing.T) { + // Exact match IPv4 + ok, err := utils.FilterIP("10.10.0.1", "10.10.0.1") + assert.NilError(t, err) + assert.Equal(t, true, ok) + + // Non-match IPv4 + ok, err = utils.FilterIP("10.10.0.1", "10.10.0.2") + assert.NilError(t, err) + assert.Equal(t, false, ok) + + // CIDR match IPv4 + ok, err = utils.FilterIP("10.10.0.0/24", "10.10.0.2") + assert.NilError(t, err) + assert.Equal(t, true, ok) + + // CIDR match IPv4 with '-' instead of '/' + ok, err = utils.FilterIP("10.10.10.0-24", "10.10.10.5") + assert.NilError(t, err) + assert.Equal(t, true, ok) + + // CIDR non-match IPv4 + ok, err = utils.FilterIP("10.10.0.0/24", "10.5.0.1") + assert.NilError(t, err) + assert.Equal(t, false, ok) + + // Invalid CIDR + ok, err = utils.FilterIP("10.10.0.0/222", "10.0.0.1") + assert.ErrorContains(t, err, "invalid CIDR address") + assert.Equal(t, false, ok) + + // Invalid IP in filter + ok, err = utils.FilterIP("invalid_ip", "10.5.5.5") + assert.ErrorContains(t, err, "invalid IP address in filter") + assert.Equal(t, false, ok) + + // Invalid IP to check + ok, err = utils.FilterIP("10.10.10.10", "invalid_ip") + assert.ErrorContains(t, err, "invalid IP address") + assert.Equal(t, false, ok) +} + +func TestCheckFilter(t *testing.T) { + // Empty filter + assert.Equal(t, true, utils.CheckFilter("", "anystring")) + + // Exact match + assert.Equal(t, true, utils.CheckFilter("hello", "hello")) + + // Regex match + assert.Equal(t, true, utils.CheckFilter("/^h.*o$/", "hello")) + + // Invalid regex + assert.Equal(t, false, utils.CheckFilter("/[unclosed", "test")) + + // Comma-separated values + assert.Equal(t, true, utils.CheckFilter("apple, banana, cherry", "banana")) + + // No match + assert.Equal(t, false, utils.CheckFilter("apple, banana, cherry", "grape")) +} + +func TestGenerateIdentifier(t *testing.T) { + // Consistent output for same input + id1 := utils.GenerateIdentifier("teststring") + id2 := utils.GenerateIdentifier("teststring") + assert.Equal(t, id1, id2) + + // Different output for different input + id3 := utils.GenerateIdentifier("differentstring") + assert.Assert(t, id1 != id3) + + // Check length (should be 8 characters from first segment of UUID) + assert.Equal(t, 8, len(id1)) +} diff --git a/internal/utils/string_utils_test.go b/internal/utils/string_utils_test.go new file mode 100644 index 00000000..3677eb62 --- /dev/null +++ b/internal/utils/string_utils_test.go @@ -0,0 +1,50 @@ +package utils_test + +import ( + "testing" + "tinyauth/internal/utils" + + "gotest.tools/v3/assert" +) + +func TestCapitalize(t *testing.T) { + // Test empty string + assert.Equal(t, "", utils.Capitalize("")) + + // Test single character + assert.Equal(t, "A", utils.Capitalize("a")) + + // Test multiple characters + assert.Equal(t, "Hello", utils.Capitalize("hello")) + + // Test already capitalized + assert.Equal(t, "World", utils.Capitalize("World")) + + // Test non-alphabetic first character + assert.Equal(t, "1number", utils.Capitalize("1number")) + + // Test Unicode characters + assert.Equal(t, "Γειά", utils.Capitalize("γειά")) + assert.Equal(t, "Привет", utils.Capitalize("привет")) + +} + +func TestCoalesceToString(t *testing.T) { + // Test with []any containing strings + assert.Equal(t, "a,b,c", utils.CoalesceToString([]any{"a", "b", "c"})) + + // Test with []any containing mixed types + assert.Equal(t, "a,c", utils.CoalesceToString([]any{"a", 1, "c", true})) + + // Test with []any containing no strings + assert.Equal(t, "", utils.CoalesceToString([]any{1, 2, 3})) + + // Test with string input + assert.Equal(t, "hello", utils.CoalesceToString("hello")) + + // Test with non-string, non-[]any input + assert.Equal(t, "", utils.CoalesceToString(123)) + + // Test with nil input + assert.Equal(t, "", utils.CoalesceToString(nil)) +} diff --git a/internal/utils/user_utils_test.go b/internal/utils/user_utils_test.go new file mode 100644 index 00000000..d04636ae --- /dev/null +++ b/internal/utils/user_utils_test.go @@ -0,0 +1,163 @@ +package utils_test + +import ( + "os" + "testing" + "tinyauth/internal/utils" + + "gotest.tools/v3/assert" +) + +func TestGetUsers(t *testing.T) { + // Setup + file, err := os.Create("/tmp/tinyauth_users_test.txt") + assert.NilError(t, err) + + _, err = file.WriteString(" user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G \n user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G ") // Spacing is on purpose + assert.NilError(t, err) + + err = file.Close() + assert.NilError(t, err) + defer os.Remove("/tmp/tinyauth_users_test.txt") + + // Test file + users, err := utils.GetUsers("", "/tmp/tinyauth_users_test.txt") + + assert.NilError(t, err) + + assert.Equal(t, 2, len(users)) + + assert.Equal(t, "user1", users[0].Username) + assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[0].Password) + assert.Equal(t, "user2", users[1].Username) + assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password) + + // Test config + users, err = utils.GetUsers("user3:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G,user4:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "") + + assert.NilError(t, err) + + assert.Equal(t, 2, len(users)) + + assert.Equal(t, "user3", users[0].Username) + assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[0].Password) + assert.Equal(t, "user4", users[1].Username) + assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password) + + // Test both + users, err = utils.GetUsers("user5:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "/tmp/tinyauth_users_test.txt") + + assert.NilError(t, err) + + assert.Equal(t, 3, len(users)) + + assert.Equal(t, "user5", users[0].Username) + assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[0].Password) + assert.Equal(t, "user1", users[1].Username) + assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password) + assert.Equal(t, "user2", users[2].Username) + assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[2].Password) + + // Test empty + users, err = utils.GetUsers("", "") + + assert.NilError(t, err) + + assert.Equal(t, 0, len(users)) + + // Test non-existent file + users, err = utils.GetUsers("", "/tmp/non_existent_file.txt") + + assert.ErrorContains(t, err, "no such file or directory") + + assert.Equal(t, 0, len(users)) +} + +func TestParseUsers(t *testing.T) { + // Valid users + users, err := utils.ParseUsers("user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G,user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF") // user2 has TOTP + + assert.NilError(t, err) + + assert.Equal(t, 2, len(users)) + + assert.Equal(t, "user1", users[0].Username) + assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[0].Password) + assert.Equal(t, "", users[0].TotpSecret) + assert.Equal(t, "user2", users[1].Username) + assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password) + assert.Equal(t, "ABCDEF", users[1].TotpSecret) + + // Valid weirdly spaced users + users, err = utils.ParseUsers(" user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G , user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF ") // Spacing is on purpose + assert.NilError(t, err) + + assert.Equal(t, 2, len(users)) + + assert.Equal(t, "user1", users[0].Username) + assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[0].Password) + assert.Equal(t, "", users[0].TotpSecret) + assert.Equal(t, "user2", users[1].Username) + assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password) + assert.Equal(t, "ABCDEF", users[1].TotpSecret) +} + +func TestParseUser(t *testing.T) { + // Valid user without TOTP + user, err := utils.ParseUser("user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G") + + assert.NilError(t, err) + + assert.Equal(t, "user1", user.Username) + assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", user.Password) + assert.Equal(t, "", user.TotpSecret) + + // Valid user with TOTP + user, err = utils.ParseUser("user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF") + + assert.NilError(t, err) + + assert.Equal(t, "user2", user.Username) + assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", user.Password) + assert.Equal(t, "ABCDEF", user.TotpSecret) + + // Valid user with $$ in password + user, err = utils.ParseUser("user3:pa$$word123") + + assert.NilError(t, err) + + assert.Equal(t, "user3", user.Username) + assert.Equal(t, "pa$word123", user.Password) + assert.Equal(t, "", user.TotpSecret) + + // User with spaces + user, err = utils.ParseUser(" user4 : password123 : TOTPSECRET ") + + assert.NilError(t, err) + + assert.Equal(t, "user4", user.Username) + assert.Equal(t, "password123", user.Password) + assert.Equal(t, "TOTPSECRET", user.TotpSecret) + + // Invalid users + _, err = utils.ParseUser("user1") // Missing password + assert.ErrorContains(t, err, "invalid user format") + + _, err = utils.ParseUser("user1:") + assert.ErrorContains(t, err, "invalid user format") + + _, err = utils.ParseUser(":password123") + assert.ErrorContains(t, err, "invalid user format") + + _, err = utils.ParseUser("user1:password123:ABC:EXTRA") // Too many parts + assert.ErrorContains(t, err, "invalid user format") + + _, err = utils.ParseUser("user1::ABC") + assert.ErrorContains(t, err, "invalid user format") + + _, err = utils.ParseUser(":password123:ABC") + assert.ErrorContains(t, err, "invalid user format") + + _, err = utils.ParseUser(" : : ") + assert.ErrorContains(t, err, "invalid user format") +} From e03eaf4f0899b13a632c27d627bd20e6c541e420 Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 10 Sep 2025 13:43:08 +0300 Subject: [PATCH 023/127] feat: add psl check in cookie domain --- frontend/src/schemas/app-context-schema.ts | 1 - go.mod | 17 ++++--- go.sum | 34 +++++++------- internal/bootstrap/app_bootstrap.go | 13 +++-- internal/controller/context_controller.go | 3 -- .../controller/context_controller_test.go | 2 - internal/controller/oauth_controller.go | 16 +++---- internal/controller/proxy_controller_test.go | 2 +- .../controller/resources_controller_test.go | 1 + internal/controller/user_controller.go | 8 ++-- internal/controller/user_controller_test.go | 4 +- internal/middleware/context_middleware.go | 6 +-- internal/service/auth_service.go | 6 +-- internal/utils/app_utils.go | 32 ++++++++----- internal/utils/app_utils_test.go | 47 ++++++++++--------- 15 files changed, 102 insertions(+), 90 deletions(-) diff --git a/frontend/src/schemas/app-context-schema.ts b/frontend/src/schemas/app-context-schema.ts index c5d6d85f..7d29c7ef 100644 --- a/frontend/src/schemas/app-context-schema.ts +++ b/frontend/src/schemas/app-context-schema.ts @@ -5,7 +5,6 @@ export const appContextSchema = z.object({ title: z.string(), genericName: z.string(), appUrl: z.string(), - rootDomain: z.string(), forgotPasswordMessage: z.string(), oauthAutoRedirect: z.enum(["none", "github", "google", "generic"]), backgroundImage: z.string(), diff --git a/go.mod b/go.mod index c4855ffb..41b9c263 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module tinyauth -go 1.23.2 +go 1.24.0 + +toolchain go1.24.3 require ( github.com/cenkalti/backoff/v5 v5.0.3 @@ -15,7 +17,8 @@ require ( github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 github.com/traefik/paerser v0.2.2 - golang.org/x/crypto v0.41.0 + github.com/weppos/publicsuffix-go v0.50.0 + golang.org/x/crypto v0.42.0 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b gorm.io/gorm v1.30.1 gotest.tools/v3 v3.5.2 @@ -45,7 +48,7 @@ require ( go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect go.opentelemetry.io/otel/sdk v1.34.0 // indirect - golang.org/x/term v0.34.0 // indirect + golang.org/x/term v0.35.0 // indirect modernc.org/libc v1.66.3 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect @@ -123,11 +126,11 @@ require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/arch v0.13.0 // indirect - golang.org/x/net v0.42.0 // indirect + golang.org/x/net v0.44.0 // indirect golang.org/x/oauth2 v0.30.0 - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.28.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect google.golang.org/protobuf v1.36.3 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6e7d9c7c..5d1645b8 100644 --- a/go.sum +++ b/go.sum @@ -280,6 +280,8 @@ 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/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/weppos/publicsuffix-go v0.50.0 h1:M178k6l8cnh9T1c1cStkhytVxdk5zPd6gGZf8ySIuVo= +github.com/weppos/publicsuffix-go v0.50.0/go.mod h1:VXhClBYMlDrUsome4pOTpe68Ui0p6iQRAbyHQD1yKoU= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -311,27 +313,27 @@ 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.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= 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/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= -golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= 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.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 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.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -339,22 +341,22 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc 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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= 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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-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/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= -golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= 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= diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index 0f967dff..211d3fe0 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -45,8 +45,8 @@ func (app *BootstrapApp) Setup() error { return err } - // Get root domain - rootDomain, err := utils.GetRootDomain(app.Config.AppURL) + // Get cookie domain + cookieDomain, err := utils.GetCookieDomain(app.Config.AppURL) if err != nil { return err @@ -65,7 +65,7 @@ func (app *BootstrapApp) Setup() error { OauthWhitelist: app.Config.OAuthWhitelist, SessionExpiry: app.Config.SessionExpiry, SecureCookie: app.Config.SecureCookie, - RootDomain: rootDomain, + CookieDomain: cookieDomain, LoginTimeout: app.Config.LoginTimeout, LoginMaxRetries: app.Config.LoginMaxRetries, SessionCookieName: sessionCookieName, @@ -156,7 +156,7 @@ func (app *BootstrapApp) Setup() error { var middlewares []Middleware contextMiddleware := middleware.NewContextMiddleware(middleware.ContextMiddlewareConfig{ - RootDomain: rootDomain, + CookieDomain: cookieDomain, }, authService, oauthBrokerService) uiMiddleware := middleware.NewUIMiddleware() @@ -183,7 +183,6 @@ func (app *BootstrapApp) Setup() error { Title: app.Config.Title, GenericName: app.Config.GenericName, AppURL: app.Config.AppURL, - RootDomain: rootDomain, ForgotPasswordMessage: app.Config.ForgotPasswordMessage, BackgroundImage: app.Config.BackgroundImage, OAuthAutoRedirect: app.Config.OAuthAutoRedirect, @@ -194,7 +193,7 @@ func (app *BootstrapApp) Setup() error { SecureCookie: app.Config.SecureCookie, CSRFCookieName: csrfCookieName, RedirectCookieName: redirectCookieName, - RootDomain: rootDomain, + CookieDomain: cookieDomain, }, apiRouter, authService, oauthBrokerService) proxyController := controller.NewProxyController(controller.ProxyControllerConfig{ @@ -202,7 +201,7 @@ func (app *BootstrapApp) Setup() error { }, apiRouter, dockerService, authService) userController := controller.NewUserController(controller.UserControllerConfig{ - RootDomain: rootDomain, + CookieDomain: cookieDomain, }, apiRouter, authService) resourcesController := controller.NewResourcesController(controller.ResourcesControllerConfig{ diff --git a/internal/controller/context_controller.go b/internal/controller/context_controller.go index d285da3c..a7bc8a54 100644 --- a/internal/controller/context_controller.go +++ b/internal/controller/context_controller.go @@ -28,7 +28,6 @@ type AppContextResponse struct { Title string `json:"title"` GenericName string `json:"genericName"` AppURL string `json:"appUrl"` - RootDomain string `json:"rootDomain"` ForgotPasswordMessage string `json:"forgotPasswordMessage"` BackgroundImage string `json:"backgroundImage"` OAuthAutoRedirect string `json:"oauthAutoRedirect"` @@ -39,7 +38,6 @@ type ContextControllerConfig struct { Title string GenericName string AppURL string - RootDomain string ForgotPasswordMessage string BackgroundImage string OAuthAutoRedirect string @@ -100,7 +98,6 @@ func (controller *ContextController) appContextHandler(c *gin.Context) { Title: controller.config.Title, GenericName: controller.config.GenericName, AppURL: fmt.Sprintf("%s://%s", appUrl.Scheme, appUrl.Host), - RootDomain: controller.config.RootDomain, ForgotPasswordMessage: controller.config.ForgotPasswordMessage, BackgroundImage: controller.config.BackgroundImage, OAuthAutoRedirect: controller.config.OAuthAutoRedirect, diff --git a/internal/controller/context_controller_test.go b/internal/controller/context_controller_test.go index af64b218..c8371f93 100644 --- a/internal/controller/context_controller_test.go +++ b/internal/controller/context_controller_test.go @@ -16,7 +16,6 @@ var controllerCfg = controller.ContextControllerConfig{ Title: "Test App", GenericName: "Generic", AppURL: "http://localhost:8080", - RootDomain: "localhost", ForgotPasswordMessage: "Contact admin to reset your password.", BackgroundImage: "/assets/bg.jpg", OAuthAutoRedirect: "google", @@ -62,7 +61,6 @@ func TestAppContextHandler(t *testing.T) { Title: controllerCfg.Title, GenericName: controllerCfg.GenericName, AppURL: controllerCfg.AppURL, - RootDomain: controllerCfg.RootDomain, ForgotPasswordMessage: controllerCfg.ForgotPasswordMessage, BackgroundImage: controllerCfg.BackgroundImage, OAuthAutoRedirect: controllerCfg.OAuthAutoRedirect, diff --git a/internal/controller/oauth_controller.go b/internal/controller/oauth_controller.go index ddf2d02b..a65b53a0 100644 --- a/internal/controller/oauth_controller.go +++ b/internal/controller/oauth_controller.go @@ -23,7 +23,7 @@ type OAuthControllerConfig struct { RedirectCookieName string SecureCookie bool AppURL string - RootDomain string + CookieDomain string } type OAuthController struct { @@ -74,13 +74,13 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) { state := service.GenerateState() authURL := service.GetAuthURL(state) - c.SetCookie(controller.config.CSRFCookieName, state, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.RootDomain), controller.config.SecureCookie, true) + c.SetCookie(controller.config.CSRFCookieName, state, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true) redirectURI := c.Query("redirect_uri") - if redirectURI != "" && utils.IsRedirectSafe(redirectURI, controller.config.RootDomain) { + if redirectURI != "" && utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain) { log.Debug().Msg("Setting redirect URI cookie") - c.SetCookie(controller.config.RedirectCookieName, redirectURI, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.RootDomain), controller.config.SecureCookie, true) + c.SetCookie(controller.config.RedirectCookieName, redirectURI, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true) } c.JSON(200, gin.H{ @@ -108,12 +108,12 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { if err != nil || state != csrfCookie { log.Warn().Err(err).Msg("CSRF token mismatch or cookie missing") - c.SetCookie(controller.config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.RootDomain), controller.config.SecureCookie, true) + c.SetCookie(controller.config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true) c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) return } - c.SetCookie(controller.config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.RootDomain), controller.config.SecureCookie, true) + c.SetCookie(controller.config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true) code := c.Query("code") service, exists := controller.broker.GetService(req.Provider) @@ -196,7 +196,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { redirectURI, err := c.Cookie(controller.config.RedirectCookieName) - if err != nil || !utils.IsRedirectSafe(redirectURI, controller.config.RootDomain) { + if err != nil || !utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain) { log.Debug().Msg("No redirect URI cookie found, redirecting to app root") c.Redirect(http.StatusTemporaryRedirect, controller.config.AppURL) return @@ -212,6 +212,6 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { return } - c.SetCookie(controller.config.RedirectCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.RootDomain), controller.config.SecureCookie, true) + c.SetCookie(controller.config.RedirectCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true) c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/continue?%s", controller.config.AppURL, queries.Encode())) } diff --git a/internal/controller/proxy_controller_test.go b/internal/controller/proxy_controller_test.go index 718eb0d4..fce2ec38 100644 --- a/internal/controller/proxy_controller_test.go +++ b/internal/controller/proxy_controller_test.go @@ -50,7 +50,7 @@ func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.En OauthWhitelist: "", SessionExpiry: 3600, SecureCookie: false, - RootDomain: "localhost", + CookieDomain: "localhost", LoginTimeout: 300, LoginMaxRetries: 3, SessionCookieName: "tinyauth-session", diff --git a/internal/controller/resources_controller_test.go b/internal/controller/resources_controller_test.go index 732b2f70..8e4f8434 100644 --- a/internal/controller/resources_controller_test.go +++ b/internal/controller/resources_controller_test.go @@ -24,6 +24,7 @@ func TestResourcesHandler(t *testing.T) { // Create test data err := os.Mkdir("/tmp/tinyauth", 0755) assert.NilError(t, err) + defer os.RemoveAll("/tmp/tinyauth") file, err := os.Create("/tmp/tinyauth/test.txt") assert.NilError(t, err) diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 7b486523..2513829b 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -22,7 +22,7 @@ type TotpRequest struct { } type UserControllerConfig struct { - RootDomain string + CookieDomain string } type UserController struct { @@ -115,7 +115,7 @@ func (controller *UserController) loginHandler(c *gin.Context) { err := controller.auth.CreateSessionCookie(c, &config.SessionCookie{ Username: user.Username, Name: utils.Capitalize(req.Username), - Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.RootDomain), + Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.CookieDomain), Provider: "username", TotpPending: true, }) @@ -141,7 +141,7 @@ func (controller *UserController) loginHandler(c *gin.Context) { err = controller.auth.CreateSessionCookie(c, &config.SessionCookie{ Username: req.Username, Name: utils.Capitalize(req.Username), - Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.RootDomain), + Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.CookieDomain), Provider: "username", }) @@ -246,7 +246,7 @@ func (controller *UserController) totpHandler(c *gin.Context) { err = controller.auth.CreateSessionCookie(c, &config.SessionCookie{ Username: user.Username, Name: utils.Capitalize(user.Username), - Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), controller.config.RootDomain), + Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), controller.config.CookieDomain), Provider: "username", }) diff --git a/internal/controller/user_controller_test.go b/internal/controller/user_controller_test.go index 93982bcc..60655218 100644 --- a/internal/controller/user_controller_test.go +++ b/internal/controller/user_controller_test.go @@ -58,7 +58,7 @@ func setupUserController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Eng OauthWhitelist: "", SessionExpiry: 3600, SecureCookie: false, - RootDomain: "localhost", + CookieDomain: "localhost", LoginTimeout: 300, LoginMaxRetries: 3, SessionCookieName: "tinyauth-session", @@ -66,7 +66,7 @@ func setupUserController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Eng // Controller ctrl := controller.NewUserController(controller.UserControllerConfig{ - RootDomain: "localhost", + CookieDomain: "localhost", }, group, authService) ctrl.SetupRoutes() diff --git a/internal/middleware/context_middleware.go b/internal/middleware/context_middleware.go index cbf94120..30fa6230 100644 --- a/internal/middleware/context_middleware.go +++ b/internal/middleware/context_middleware.go @@ -12,7 +12,7 @@ import ( ) type ContextMiddlewareConfig struct { - RootDomain string + CookieDomain string } type ContextMiddleware struct { @@ -134,7 +134,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { c.Set("context", &config.UserContext{ Username: user.Username, Name: utils.Capitalize(user.Username), - Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), m.config.RootDomain), + Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), m.config.CookieDomain), Provider: "basic", IsLoggedIn: true, TotpEnabled: user.TotpSecret != "", @@ -146,7 +146,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { c.Set("context", &config.UserContext{ Username: basic.Username, Name: utils.Capitalize(basic.Username), - Email: fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), m.config.RootDomain), + Email: fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), m.config.CookieDomain), Provider: "basic", IsLoggedIn: true, }) diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index 9739cb93..a3f8ed01 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -28,7 +28,7 @@ type AuthServiceConfig struct { OauthWhitelist string SessionExpiry int SecureCookie bool - RootDomain string + CookieDomain string LoginTimeout int LoginMaxRetries int SessionCookieName string @@ -218,7 +218,7 @@ func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.Sessio return err } - c.SetCookie(auth.config.SessionCookieName, session.UUID, expiry, "/", fmt.Sprintf(".%s", auth.config.RootDomain), auth.config.SecureCookie, true) + c.SetCookie(auth.config.SessionCookieName, session.UUID, expiry, "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true) return nil } @@ -236,7 +236,7 @@ func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error { return res.Error } - c.SetCookie(auth.config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.config.RootDomain), auth.config.SecureCookie, true) + c.SetCookie(auth.config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true) return nil } diff --git a/internal/utils/app_utils.go b/internal/utils/app_utils.go index 57e40f4e..c4b98c6b 100644 --- a/internal/utils/app_utils.go +++ b/internal/utils/app_utils.go @@ -8,30 +8,38 @@ import ( "tinyauth/internal/config" "github.com/gin-gonic/gin" - "github.com/rs/zerolog" + "github.com/weppos/publicsuffix-go/publicsuffix" ) -// Get root domain parses a hostname and returns the upper domain (e.g. sub1.sub2.domain.com -> sub2.domain.com) -func GetRootDomain(u string) (string, error) { - appUrl, err := url.Parse(u) +// Get cookie domain parses a hostname and returns the upper domain (e.g. sub1.sub2.domain.com -> sub2.domain.com) +func GetCookieDomain(u string) (string, error) { + parsed, err := url.Parse(u) if err != nil { return "", err } - host := appUrl.Hostname() + host := parsed.Hostname() if netIP := net.ParseIP(host); netIP != nil { - return "", errors.New("IP addresses are not allowed") + return "", errors.New("IP addresses not allowed") + } + + parts := strings.Split(host, ".") + + if len(parts) < 3 { + return "", errors.New("invalid app url, must be at least second level domain") } - urlParts := strings.Split(host, ".") + domain := strings.Join(parts[1:], ".") + + _, err = publicsuffix.DomainFromListWithOptions(publicsuffix.DefaultList, domain, nil) - if len(urlParts) < 3 { - return "", errors.New("invalid domain, must be at least second level domain") + if err != nil { + return "", errors.New("domain in public suffix list, cannot set cookies") } - return strings.Join(urlParts[1:], "."), nil + return domain, nil } func ParseFileToLine(content string) string { @@ -89,13 +97,13 @@ func IsRedirectSafe(redirectURL string, domain string) bool { return false } - upper, err := GetRootDomain(redirectURL) + cookieDomain, err := GetCookieDomain(redirectURL) if err != nil { return false } - if upper != domain { + if cookieDomain != domain { return false } diff --git a/internal/utils/app_utils_test.go b/internal/utils/app_utils_test.go index 1540c76c..c35db3d7 100644 --- a/internal/utils/app_utils_test.go +++ b/internal/utils/app_utils_test.go @@ -11,53 +11,58 @@ import ( func TestGetRootDomain(t *testing.T) { // Normal case - domain := "http://sub.example.com" - expected := "example.com" - result, err := utils.GetRootDomain(domain) + domain := "http://sub.tinyauth.app" + expected := "tinyauth.app" + result, err := utils.GetCookieDomain(domain) assert.NilError(t, err) assert.Equal(t, expected, result) // Domain with multiple subdomains - domain = "http://b.c.example.com" - expected = "c.example.com" - result, err = utils.GetRootDomain(domain) + domain = "http://b.c.tinyauth.app" + expected = "c.tinyauth.app" + result, err = utils.GetCookieDomain(domain) assert.NilError(t, err) assert.Equal(t, expected, result) // Domain with no subdomain - domain = "http://example.com" - expected = "example.com" - _, err = utils.GetRootDomain(domain) - assert.Error(t, err, "invalid domain, must be at least second level domain") + domain = "http://tinyauth.app" + expected = "tinyauth.app" + _, err = utils.GetCookieDomain(domain) + assert.Error(t, err, "invalid app url, must be at least second level domain") // Invalid domain (only TLD) domain = "com" - _, err = utils.GetRootDomain(domain) - assert.ErrorContains(t, err, "invalid domain") + _, err = utils.GetCookieDomain(domain) + assert.ErrorContains(t, err, "invalid app url, must be at least second level domain") // IP address domain = "http://10.10.10.10" - _, err = utils.GetRootDomain(domain) - assert.ErrorContains(t, err, "IP addresses are not allowed") + _, err = utils.GetCookieDomain(domain) + assert.ErrorContains(t, err, "IP addresses not allowed") // Invalid URL domain = "http://[::1]:namedport" - _, err = utils.GetRootDomain(domain) + _, err = utils.GetCookieDomain(domain) assert.ErrorContains(t, err, "parse \"http://[::1]:namedport\": invalid port \":namedport\" after host") // URL with scheme and path - domain = "https://sub.example.com/path" - expected = "example.com" - result, err = utils.GetRootDomain(domain) + domain = "https://sub.tinyauth.app/path" + expected = "tinyauth.app" + result, err = utils.GetCookieDomain(domain) assert.NilError(t, err) assert.Equal(t, expected, result) // URL with port - domain = "http://sub.example.com:8080" - expected = "example.com" - result, err = utils.GetRootDomain(domain) + domain = "http://sub.tinyauth.app:8080" + expected = "tinyauth.app" + result, err = utils.GetCookieDomain(domain) assert.NilError(t, err) assert.Equal(t, expected, result) + + // Domain managed by ICANN + domain = "http://example.co.uk" + _, err = utils.GetCookieDomain(domain) + assert.Error(t, err, "domain in public suffix list, cannot set cookies") } func TestParseFileToLine(t *testing.T) { From 2d78e6b598dfbebd2b89bd6870a8afd2d64c57e2 Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 10 Sep 2025 13:47:48 +0300 Subject: [PATCH 024/127] feat: add cookie domain back to context controller --- frontend/src/lib/i18n/locales/en-US.json | 2 +- frontend/src/lib/i18n/locales/en.json | 2 +- frontend/src/pages/continue-page.tsx | 8 ++++---- frontend/src/schemas/app-context-schema.ts | 1 + internal/bootstrap/app_bootstrap.go | 1 + internal/controller/context_controller.go | 3 +++ internal/controller/context_controller_test.go | 2 ++ 7 files changed, 13 insertions(+), 6 deletions(-) diff --git a/frontend/src/lib/i18n/locales/en-US.json b/frontend/src/lib/i18n/locales/en-US.json index b2dd9001..6338a884 100644 --- a/frontend/src/lib/i18n/locales/en-US.json +++ b/frontend/src/lib/i18n/locales/en-US.json @@ -21,7 +21,7 @@ "continueInsecureRedirectTitle": "Insecure redirect", "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", "continueUntrustedRedirectTitle": "Untrusted redirect", - "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{rootDomain}}). Are you sure you want to continue?", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", "logoutFailTitle": "Failed to log out", "logoutFailSubtitle": "Please try again", "logoutSuccessTitle": "Logged out", diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index b2dd9001..6338a884 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -21,7 +21,7 @@ "continueInsecureRedirectTitle": "Insecure redirect", "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", "continueUntrustedRedirectTitle": "Untrusted redirect", - "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{rootDomain}}). Are you sure you want to continue?", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", "logoutFailTitle": "Failed to log out", "logoutFailSubtitle": "Please try again", "logoutSuccessTitle": "Logged out", diff --git a/frontend/src/pages/continue-page.tsx b/frontend/src/pages/continue-page.tsx index 261be8b8..f17bd975 100644 --- a/frontend/src/pages/continue-page.tsx +++ b/frontend/src/pages/continue-page.tsx @@ -14,7 +14,7 @@ import { Navigate, useLocation, useNavigate } from "react-router"; import { useEffect, useState } from "react"; export const ContinuePage = () => { - const { rootDomain } = useAppContext(); + const { cookieDomain } = useAppContext(); const { isLoggedIn } = useUserContext(); const { search } = useLocation(); const { t } = useTranslation(); @@ -33,8 +33,8 @@ export const ContinuePage = () => { : null; const isTrustedRedirectUri = redirectUriObj !== null - ? redirectUriObj.hostname === rootDomain || - redirectUriObj.hostname.endsWith(`.${rootDomain}`) + ? redirectUriObj.hostname === cookieDomain || + redirectUriObj.hostname.endsWith(`.${cookieDomain}`) : false; const isAllowedRedirectProto = redirectUriObj !== null @@ -105,7 +105,7 @@ export const ContinuePage = () => { components={{ code: , }} - values={{ rootDomain }} + values={{ cookieDomain }} /> diff --git a/frontend/src/schemas/app-context-schema.ts b/frontend/src/schemas/app-context-schema.ts index 7d29c7ef..8931be1b 100644 --- a/frontend/src/schemas/app-context-schema.ts +++ b/frontend/src/schemas/app-context-schema.ts @@ -5,6 +5,7 @@ export const appContextSchema = z.object({ title: z.string(), genericName: z.string(), appUrl: z.string(), + cookieDomain: z.string(), forgotPasswordMessage: z.string(), oauthAutoRedirect: z.enum(["none", "github", "google", "generic"]), backgroundImage: z.string(), diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index 211d3fe0..db2e564c 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -183,6 +183,7 @@ func (app *BootstrapApp) Setup() error { Title: app.Config.Title, GenericName: app.Config.GenericName, AppURL: app.Config.AppURL, + CookieDomain: cookieDomain, ForgotPasswordMessage: app.Config.ForgotPasswordMessage, BackgroundImage: app.Config.BackgroundImage, OAuthAutoRedirect: app.Config.OAuthAutoRedirect, diff --git a/internal/controller/context_controller.go b/internal/controller/context_controller.go index a7bc8a54..ee3eec69 100644 --- a/internal/controller/context_controller.go +++ b/internal/controller/context_controller.go @@ -28,6 +28,7 @@ type AppContextResponse struct { Title string `json:"title"` GenericName string `json:"genericName"` AppURL string `json:"appUrl"` + CookieDomain string `json:"cookieDomain"` ForgotPasswordMessage string `json:"forgotPasswordMessage"` BackgroundImage string `json:"backgroundImage"` OAuthAutoRedirect string `json:"oauthAutoRedirect"` @@ -38,6 +39,7 @@ type ContextControllerConfig struct { Title string GenericName string AppURL string + CookieDomain string ForgotPasswordMessage string BackgroundImage string OAuthAutoRedirect string @@ -98,6 +100,7 @@ func (controller *ContextController) appContextHandler(c *gin.Context) { Title: controller.config.Title, GenericName: controller.config.GenericName, AppURL: fmt.Sprintf("%s://%s", appUrl.Scheme, appUrl.Host), + CookieDomain: controller.config.CookieDomain, ForgotPasswordMessage: controller.config.ForgotPasswordMessage, BackgroundImage: controller.config.BackgroundImage, OAuthAutoRedirect: controller.config.OAuthAutoRedirect, diff --git a/internal/controller/context_controller_test.go b/internal/controller/context_controller_test.go index c8371f93..44f77a17 100644 --- a/internal/controller/context_controller_test.go +++ b/internal/controller/context_controller_test.go @@ -16,6 +16,7 @@ var controllerCfg = controller.ContextControllerConfig{ Title: "Test App", GenericName: "Generic", AppURL: "http://localhost:8080", + CookieDomain: "localhost", ForgotPasswordMessage: "Contact admin to reset your password.", BackgroundImage: "/assets/bg.jpg", OAuthAutoRedirect: "google", @@ -61,6 +62,7 @@ func TestAppContextHandler(t *testing.T) { Title: controllerCfg.Title, GenericName: controllerCfg.GenericName, AppURL: controllerCfg.AppURL, + CookieDomain: controllerCfg.CookieDomain, ForgotPasswordMessage: controllerCfg.ForgotPasswordMessage, BackgroundImage: controllerCfg.BackgroundImage, OAuthAutoRedirect: controllerCfg.OAuthAutoRedirect, From 5c866bad1ad6227b1cf9c1be4cead6d5c4be680e Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 16 Sep 2025 13:28:28 +0300 Subject: [PATCH 025/127] feat: multiple oauth providers (#355) * feat: add flag decoder (candidate) * refactor: finalize flags decoder * feat: add env decoder * feat: add oauth config parsing logic * feat: implement backend logic for multiple oauth providers * feat: implement multiple oauth providers in the frontend * feat: add some default icons * chore: add credits for parser * feat: style oauth auto redirect screen * fix: bot suggestions * refactor: rework decoders using simpler and more efficient pattern * refactor: rework oauth name database migration --- cmd/root.go | 23 +--- frontend/src/components/icons/microsoft.tsx | 18 +++ .../icons/{generic.tsx => oauth.tsx} | 2 +- frontend/src/components/icons/pocket-id.tsx | 20 +++ frontend/src/components/icons/tailscale.tsx | 26 ++++ frontend/src/lib/i18n/locales/en-US.json | 3 + frontend/src/lib/i18n/locales/en.json | 3 + frontend/src/pages/continue-page.tsx | 2 +- frontend/src/pages/login-page.tsx | 120 +++++++++++------- frontend/src/pages/logout-page.tsx | 8 +- frontend/src/schemas/app-context-schema.ts | 11 +- frontend/src/schemas/user-context-schema.ts | 1 + .../migrations/000002_oauth_name.down.sql | 1 + .../migrations/000002_oauth_name.up.sql | 10 ++ internal/bootstrap/app_bootstrap.go | 74 ++++++----- internal/config/config.go | 89 ++++++------- internal/controller/context_controller.go | 33 +++-- .../controller/context_controller_test.go | 17 ++- internal/controller/oauth_controller.go | 1 + internal/middleware/context_middleware.go | 1 + internal/model/session_model.go | 1 + internal/service/auth_service.go | 2 + internal/service/generic_oauth_service.go | 6 + internal/service/github_oauth_service.go | 6 + internal/service/google_oauth_service.go | 6 + internal/service/oauth_broker_service.go | 1 + internal/utils/app_utils.go | 68 ++++++++++ internal/utils/app_utils_test.go | 69 ++++++++++ internal/utils/decoders/decoders.go | 81 ++++++++++++ internal/utils/decoders/decoders_test.go | 44 +++++++ internal/utils/decoders/env_decoder.go | 20 +++ internal/utils/decoders/env_decoder_test.go | 60 +++++++++ internal/utils/decoders/flags_decoder.go | 30 +++++ internal/utils/decoders/flags_decoder_test.go | 60 +++++++++ internal/utils/decoders/label_decoder_test.go | 13 +- 35 files changed, 744 insertions(+), 186 deletions(-) create mode 100644 frontend/src/components/icons/microsoft.tsx rename frontend/src/components/icons/{generic.tsx => oauth.tsx} (91%) create mode 100644 frontend/src/components/icons/pocket-id.tsx create mode 100644 frontend/src/components/icons/tailscale.tsx create mode 100644 internal/assets/migrations/000002_oauth_name.down.sql create mode 100644 internal/assets/migrations/000002_oauth_name.up.sql create mode 100644 internal/utils/decoders/decoders.go create mode 100644 internal/utils/decoders/decoders_test.go create mode 100644 internal/utils/decoders/env_decoder.go create mode 100644 internal/utils/decoders/env_decoder_test.go create mode 100644 internal/utils/decoders/flags_decoder.go create mode 100644 internal/utils/decoders/flags_decoder_test.go diff --git a/cmd/root.go b/cmd/root.go index 155ccd2f..aeb96a59 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -27,11 +27,6 @@ var rootCmd = &cobra.Command{ log.Fatal().Err(err).Msg("Failed to parse config") } - // Check if secrets have a file associated with them - conf.GithubClientSecret = utils.GetSecret(conf.GithubClientSecret, conf.GithubClientSecretFile) - conf.GoogleClientSecret = utils.GetSecret(conf.GoogleClientSecret, conf.GoogleClientSecretFile) - conf.GenericClientSecret = utils.GetSecret(conf.GenericClientSecret, conf.GenericClientSecretFile) - // Validate config v := validator.New() @@ -57,6 +52,7 @@ var rootCmd = &cobra.Command{ } func Execute() { + rootCmd.FParseErrWhitelist.UnknownFlags = true err := rootCmd.Execute() if err != nil { log.Fatal().Err(err).Msg("Failed to execute command") @@ -80,21 +76,6 @@ func init() { {"users", "", "Comma separated list of users in the format username:hash."}, {"users-file", "", "Path to a file containing users in the format username:hash."}, {"secure-cookie", false, "Send cookie over secure connection only."}, - {"github-client-id", "", "Github OAuth client ID."}, - {"github-client-secret", "", "Github OAuth client secret."}, - {"github-client-secret-file", "", "Github OAuth client secret file."}, - {"google-client-id", "", "Google OAuth client ID."}, - {"google-client-secret", "", "Google OAuth client secret."}, - {"google-client-secret-file", "", "Google OAuth client secret file."}, - {"generic-client-id", "", "Generic OAuth client ID."}, - {"generic-client-secret", "", "Generic OAuth client secret."}, - {"generic-client-secret-file", "", "Generic OAuth client secret file."}, - {"generic-scopes", "", "Generic OAuth scopes."}, - {"generic-auth-url", "", "Generic OAuth auth URL."}, - {"generic-token-url", "", "Generic OAuth token URL."}, - {"generic-user-url", "", "Generic OAuth user info URL."}, - {"generic-name", "Generic", "Generic OAuth provider name."}, - {"generic-skip-ssl", false, "Skip SSL verification for the generic OAuth provider."}, {"oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth."}, {"oauth-auto-redirect", "none", "Auto redirect to the specified OAuth provider if configured. (available providers: github, google, generic)"}, {"session-expiry", 86400, "Session (cookie) expiration time in seconds."}, @@ -112,7 +93,7 @@ func init() { {"ldap-search-filter", "(uid=%s)", "LDAP search filter for user lookup."}, {"resources-dir", "/data/resources", "Path to a directory containing custom resources (e.g. background image)."}, {"database-path", "/data/tinyauth.db", "Path to the Sqlite database file."}, - {"trusted-proxies", "", "Comma separated list of trusted proxies (IP addresses) for correct client IP detection and for header ACLs."}, + {"trusted-proxies", "", "Comma separated list of trusted proxies (IP addresses or CIDRs) for correct client IP detection."}, } for _, opt := range configOptions { diff --git a/frontend/src/components/icons/microsoft.tsx b/frontend/src/components/icons/microsoft.tsx new file mode 100644 index 00000000..58d470c5 --- /dev/null +++ b/frontend/src/components/icons/microsoft.tsx @@ -0,0 +1,18 @@ +import type { SVGProps } from "react"; + +export function MicrosoftIcon(props: SVGProps) { + return ( + + + + + + + ); +} diff --git a/frontend/src/components/icons/generic.tsx b/frontend/src/components/icons/oauth.tsx similarity index 91% rename from frontend/src/components/icons/generic.tsx rename to frontend/src/components/icons/oauth.tsx index 6be8289c..3ca531d6 100644 --- a/frontend/src/components/icons/generic.tsx +++ b/frontend/src/components/icons/oauth.tsx @@ -1,6 +1,6 @@ import type { SVGProps } from "react"; -export function GenericIcon(props: SVGProps) { +export function OAuthIcon(props: SVGProps) { return ( ) { + return ( + + + + + ); +} diff --git a/frontend/src/components/icons/tailscale.tsx b/frontend/src/components/icons/tailscale.tsx new file mode 100644 index 00000000..9381b5cd --- /dev/null +++ b/frontend/src/components/icons/tailscale.tsx @@ -0,0 +1,26 @@ +import type { SVGProps } from "react"; + +export function TailscaleIcon(props: SVGProps) { + return ( + + + + + + ); +} diff --git a/frontend/src/lib/i18n/locales/en-US.json b/frontend/src/lib/i18n/locales/en-US.json index 6338a884..43004285 100644 --- a/frontend/src/lib/i18n/locales/en-US.json +++ b/frontend/src/lib/i18n/locales/en-US.json @@ -14,6 +14,9 @@ "loginOauthFailSubtitle": "Failed to get OAuth URL", "loginOauthSuccessTitle": "Redirecting", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", "continueTitle": "Continue", "continueRedirectingTitle": "Redirecting...", "continueRedirectingSubtitle": "You should be redirected to the app soon", diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index 6338a884..43004285 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -14,6 +14,9 @@ "loginOauthFailSubtitle": "Failed to get OAuth URL", "loginOauthSuccessTitle": "Redirecting", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", "continueTitle": "Continue", "continueRedirectingTitle": "Redirecting...", "continueRedirectingSubtitle": "You should be redirected to the app soon", diff --git a/frontend/src/pages/continue-page.tsx b/frontend/src/pages/continue-page.tsx index f17bd975..dd03a4c1 100644 --- a/frontend/src/pages/continue-page.tsx +++ b/frontend/src/pages/continue-page.tsx @@ -70,7 +70,7 @@ export const ContinuePage = () => { const reveal = setTimeout(() => { setLoading(false); setShowRedirectButton(true); - }, 1000); + }, 5000); return () => { clearTimeout(auto); diff --git a/frontend/src/pages/login-page.tsx b/frontend/src/pages/login-page.tsx index fd7108cb..2f3bc99f 100644 --- a/frontend/src/pages/login-page.tsx +++ b/frontend/src/pages/login-page.tsx @@ -1,13 +1,18 @@ import { LoginForm } from "@/components/auth/login-form"; -import { GenericIcon } from "@/components/icons/generic"; import { GithubIcon } from "@/components/icons/github"; import { GoogleIcon } from "@/components/icons/google"; +import { MicrosoftIcon } from "@/components/icons/microsoft"; +import { OAuthIcon } from "@/components/icons/oauth"; +import { PocketIDIcon } from "@/components/icons/pocket-id"; +import { TailscaleIcon } from "@/components/icons/tailscale"; +import { Button } from "@/components/ui/button"; import { Card, CardHeader, CardTitle, CardDescription, CardContent, + CardFooter, } from "@/components/ui/card"; import { OAuthButton } from "@/components/ui/oauth-button"; import { SeperatorWithChildren } from "@/components/ui/separator"; @@ -17,28 +22,40 @@ import { useIsMounted } from "@/lib/hooks/use-is-mounted"; import { LoginSchema } from "@/schemas/login-schema"; import { useMutation } from "@tanstack/react-query"; import axios, { AxiosError } from "axios"; -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Navigate, useLocation } from "react-router"; import { toast } from "sonner"; +const iconMap: Record = { + google: , + github: , + tailscale: , + microsoft: , + pocketid: , +}; + export const LoginPage = () => { const { isLoggedIn } = useUserContext(); - const { configuredProviders, title, oauthAutoRedirect, genericName } = - useAppContext(); + const { providers, title, oauthAutoRedirect } = useAppContext(); const { search } = useLocation(); const { t } = useTranslation(); const isMounted = useIsMounted(); + const [oauthAutoRedirectHandover, setOauthAutoRedirectHandover] = + useState(false); + const [showRedirectButton, setShowRedirectButton] = useState(false); const redirectTimer = useRef(null); + const redirectButtonTimer = useRef(null); const searchParams = new URLSearchParams(search); const redirectUri = searchParams.get("redirect_uri"); - const oauthConfigured = - configuredProviders.filter((provider) => provider !== "username").length > - 0; - const userAuthConfigured = configuredProviders.includes("username"); + const oauthProviders = providers.filter( + (provider) => provider.id !== "username", + ); + const userAuthConfigured = + providers.find((provider) => provider.id === "username") !== undefined; const oauthMutation = useMutation({ mutationFn: (provider: string) => @@ -56,6 +73,7 @@ export const LoginPage = () => { }, 500); }, onError: () => { + setOauthAutoRedirectHandover(false); toast.error(t("loginOauthFailTitle"), { description: t("loginOauthFailSubtitle"), }); @@ -96,12 +114,16 @@ export const LoginPage = () => { useEffect(() => { if (isMounted()) { if ( - oauthConfigured && - configuredProviders.includes(oauthAutoRedirect) && + oauthProviders.length !== 0 && + providers.find((provider) => provider.id === oauthAutoRedirect) && !isLoggedIn && redirectUri ) { + setOauthAutoRedirectHandover(true); oauthMutation.mutate(oauthAutoRedirect); + redirectButtonTimer.current = window.setTimeout(() => { + setShowRedirectButton(true); + }, 5000); } } }, []); @@ -109,6 +131,8 @@ export const LoginPage = () => { useEffect( () => () => { if (redirectTimer.current) clearTimeout(redirectTimer.current); + if (redirectButtonTimer.current) + clearTimeout(redirectButtonTimer.current); }, [], ); @@ -126,61 +150,63 @@ export const LoginPage = () => { return ; } + if (oauthAutoRedirectHandover) { + return ( + + + + {t("loginOauthAutoRedirectTitle")} + + + {t("loginOauthAutoRedirectSubtitle")} + + + {showRedirectButton && ( + + + + )} + + ); + } return ( {title} - {configuredProviders.length > 0 && ( + {providers.length > 0 && ( - {oauthConfigured ? t("loginTitle") : t("loginTitleSimple")} + {oauthProviders.length !== 0 + ? t("loginTitle") + : t("loginTitleSimple")} )} - {oauthConfigured && ( + {oauthProviders.length !== 0 && (
- {configuredProviders.includes("google") && ( - } - className="w-full" - onClick={() => oauthMutation.mutate("google")} - loading={ - oauthMutation.isPending && - oauthMutation.variables === "google" - } - disabled={oauthMutation.isPending || loginMutation.isPending} - /> - )} - {configuredProviders.includes("github") && ( - } - className="w-full" - onClick={() => oauthMutation.mutate("github")} - loading={ - oauthMutation.isPending && - oauthMutation.variables === "github" - } - disabled={oauthMutation.isPending || loginMutation.isPending} - /> - )} - {configuredProviders.includes("generic") && ( + {oauthProviders.map((provider) => ( } + key={provider.id} + title={provider.name} + icon={iconMap[provider.id] ?? } className="w-full" - onClick={() => oauthMutation.mutate("generic")} + onClick={() => oauthMutation.mutate(provider.id)} loading={ oauthMutation.isPending && - oauthMutation.variables === "generic" + oauthMutation.variables === provider.id } disabled={oauthMutation.isPending || loginMutation.isPending} /> - )} + ))}
)} - {userAuthConfigured && oauthConfigured && ( + {userAuthConfigured && oauthProviders.length !== 0 && ( {t("loginDivider")} )} {userAuthConfigured && ( @@ -189,7 +215,7 @@ export const LoginPage = () => { loading={loginMutation.isPending || oauthMutation.isPending} /> )} - {configuredProviders.length == 0 && ( + {providers.length == 0 && (

{t("failedToFetchProvidersTitle")}

diff --git a/frontend/src/pages/logout-page.tsx b/frontend/src/pages/logout-page.tsx index 17693bb6..480d8ae5 100644 --- a/frontend/src/pages/logout-page.tsx +++ b/frontend/src/pages/logout-page.tsx @@ -6,9 +6,7 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { useAppContext } from "@/context/app-context"; import { useUserContext } from "@/context/user-context"; -import { capitalize } from "@/lib/utils"; import { useMutation } from "@tanstack/react-query"; import axios from "axios"; import { useEffect, useRef } from "react"; @@ -17,8 +15,7 @@ import { Navigate } from "react-router"; import { toast } from "sonner"; export const LogoutPage = () => { - const { provider, username, isLoggedIn, email } = useUserContext(); - const { genericName } = useAppContext(); + const { provider, username, isLoggedIn, email, oauthName } = useUserContext(); const { t } = useTranslation(); const redirectTimer = useRef(null); @@ -67,8 +64,7 @@ export const LogoutPage = () => { }} values={{ username: email, - provider: - provider === "generic" ? genericName : capitalize(provider), + provider: oauthName, }} /> ) : ( diff --git a/frontend/src/schemas/app-context-schema.ts b/frontend/src/schemas/app-context-schema.ts index 8931be1b..ec766ee0 100644 --- a/frontend/src/schemas/app-context-schema.ts +++ b/frontend/src/schemas/app-context-schema.ts @@ -1,14 +1,19 @@ import { z } from "zod"; +export const providerSchema = z.object({ + id: z.string(), + name: z.string(), + oauth: z.boolean(), +}); + export const appContextSchema = z.object({ - configuredProviders: z.array(z.string()), + providers: z.array(providerSchema), title: z.string(), - genericName: z.string(), appUrl: z.string(), cookieDomain: z.string(), forgotPasswordMessage: z.string(), - oauthAutoRedirect: z.enum(["none", "github", "google", "generic"]), backgroundImage: z.string(), + oauthAutoRedirect: z.string(), }); export type AppContextSchema = z.infer; diff --git a/frontend/src/schemas/user-context-schema.ts b/frontend/src/schemas/user-context-schema.ts index ee6682cf..e7e057ac 100644 --- a/frontend/src/schemas/user-context-schema.ts +++ b/frontend/src/schemas/user-context-schema.ts @@ -8,6 +8,7 @@ export const userContextSchema = z.object({ provider: z.string(), oauth: z.boolean(), totpPending: z.boolean(), + oauthName: z.string(), }); export type UserContextSchema = z.infer; diff --git a/internal/assets/migrations/000002_oauth_name.down.sql b/internal/assets/migrations/000002_oauth_name.down.sql new file mode 100644 index 00000000..75ce3b06 --- /dev/null +++ b/internal/assets/migrations/000002_oauth_name.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "oauth_name"; \ No newline at end of file diff --git a/internal/assets/migrations/000002_oauth_name.up.sql b/internal/assets/migrations/000002_oauth_name.up.sql new file mode 100644 index 00000000..416bd295 --- /dev/null +++ b/internal/assets/migrations/000002_oauth_name.up.sql @@ -0,0 +1,10 @@ +ALTER TABLE "sessions" ADD COLUMN "oauth_name" TEXT; + +UPDATE "sessions" +SET "oauth_name" = CASE + WHEN LOWER("provider") = 'github' THEN 'GitHub' + WHEN LOWER("provider") = 'google' THEN 'Google' + ELSE UPPER(SUBSTR("provider", 1, 1)) || SUBSTR("provider", 2) +END +WHERE "oauth_name" IS NULL AND "provider" IS NOT NULL; + diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index db2e564c..5301a768 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -3,6 +3,7 @@ package bootstrap import ( "fmt" "net/url" + "os" "strings" "tinyauth/internal/config" "tinyauth/internal/controller" @@ -45,6 +46,13 @@ func (app *BootstrapApp) Setup() error { return err } + // Get OAuth configs + oauthProviders, err := utils.GetOAuthProvidersConfig(os.Environ(), os.Args, app.Config.AppURL) + + if err != nil { + return err + } + // Get cookie domain cookieDomain, err := utils.GetCookieDomain(app.Config.AppURL) @@ -112,7 +120,7 @@ func (app *BootstrapApp) Setup() error { // Create services dockerService := service.NewDockerService() authService := service.NewAuthService(authConfig, dockerService, ldapService, database) - oauthBrokerService := service.NewOAuthBrokerService(app.getOAuthBrokerConfig()) + oauthBrokerService := service.NewOAuthBrokerService(oauthProviders) // Initialize services services := []Service{ @@ -132,13 +140,41 @@ func (app *BootstrapApp) Setup() error { } // Configured providers - var configuredProviders []string + babysit := map[string]string{ + "google": "Google", + "github": "GitHub", + } + configuredProviders := make([]controller.Provider, 0) + + for id, provider := range oauthProviders { + if id == "" { + continue + } + + if provider.Name == "" { + if name, ok := babysit[id]; ok { + provider.Name = name + } else { + provider.Name = utils.Capitalize(id) + } + } + + configuredProviders = append(configuredProviders, controller.Provider{ + Name: provider.Name, + ID: id, + OAuth: true, + }) + } if authService.UserAuthConfigured() || ldapService != nil { - configuredProviders = append(configuredProviders, "username") + configuredProviders = append(configuredProviders, controller.Provider{ + Name: "Username", + ID: "username", + OAuth: false, + }) } - configuredProviders = append(configuredProviders, oauthBrokerService.GetConfiguredServices()...) + log.Debug().Interface("providers", configuredProviders).Msg("Authentication providers") if len(configuredProviders) == 0 { return fmt.Errorf("no authentication providers configured") @@ -179,9 +215,8 @@ func (app *BootstrapApp) Setup() error { // Create controllers contextController := controller.NewContextController(controller.ContextControllerConfig{ - ConfiguredProviders: configuredProviders, + Providers: configuredProviders, Title: app.Config.Title, - GenericName: app.Config.GenericName, AppURL: app.Config.AppURL, CookieDomain: cookieDomain, ForgotPasswordMessage: app.Config.ForgotPasswordMessage, @@ -235,30 +270,3 @@ func (app *BootstrapApp) Setup() error { return nil } - -// Temporary -func (app *BootstrapApp) getOAuthBrokerConfig() map[string]config.OAuthServiceConfig { - return map[string]config.OAuthServiceConfig{ - "google": { - ClientID: app.Config.GoogleClientId, - ClientSecret: app.Config.GoogleClientSecret, - RedirectURL: fmt.Sprintf("%s/api/oauth/callback/google", app.Config.AppURL), - }, - "github": { - ClientID: app.Config.GithubClientId, - ClientSecret: app.Config.GithubClientSecret, - RedirectURL: fmt.Sprintf("%s/api/oauth/callback/github", app.Config.AppURL), - }, - "generic": { - ClientID: app.Config.GenericClientId, - ClientSecret: app.Config.GenericClientSecret, - RedirectURL: fmt.Sprintf("%s/api/oauth/callback/generic", app.Config.AppURL), - Scopes: strings.Split(app.Config.GenericScopes, ","), - AuthURL: app.Config.GenericAuthURL, - TokenURL: app.Config.GenericTokenURL, - UserinfoURL: app.Config.GenericUserURL, - InsecureSkipVerify: app.Config.GenericSkipSSL, - }, - } - -} diff --git a/internal/config/config.go b/internal/config/config.go index 7ccedd3b..4fc66fcd 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -15,45 +15,30 @@ var RedirectCookieName = "tinyauth-redirect" // Main app config type Config struct { - Port int `mapstructure:"port" validate:"required"` - Address string `validate:"required,ip4_addr" mapstructure:"address"` - AppURL string `validate:"required,url" mapstructure:"app-url"` - Users string `mapstructure:"users"` - UsersFile string `mapstructure:"users-file"` - SecureCookie bool `mapstructure:"secure-cookie"` - GithubClientId string `mapstructure:"github-client-id"` - GithubClientSecret string `mapstructure:"github-client-secret"` - GithubClientSecretFile string `mapstructure:"github-client-secret-file"` - GoogleClientId string `mapstructure:"google-client-id"` - GoogleClientSecret string `mapstructure:"google-client-secret"` - GoogleClientSecretFile string `mapstructure:"google-client-secret-file"` - GenericClientId string `mapstructure:"generic-client-id"` - GenericClientSecret string `mapstructure:"generic-client-secret"` - GenericClientSecretFile string `mapstructure:"generic-client-secret-file"` - GenericScopes string `mapstructure:"generic-scopes"` - GenericAuthURL string `mapstructure:"generic-auth-url"` - GenericTokenURL string `mapstructure:"generic-token-url"` - GenericUserURL string `mapstructure:"generic-user-url"` - GenericName string `mapstructure:"generic-name"` - GenericSkipSSL bool `mapstructure:"generic-skip-ssl"` - OAuthWhitelist string `mapstructure:"oauth-whitelist"` - OAuthAutoRedirect string `mapstructure:"oauth-auto-redirect" validate:"oneof=none github google generic"` - SessionExpiry int `mapstructure:"session-expiry"` - LogLevel string `mapstructure:"log-level" validate:"oneof=trace debug info warn error fatal panic"` - Title string `mapstructure:"app-title"` - LoginTimeout int `mapstructure:"login-timeout"` - LoginMaxRetries int `mapstructure:"login-max-retries"` - ForgotPasswordMessage string `mapstructure:"forgot-password-message"` - BackgroundImage string `mapstructure:"background-image" validate:"required"` - LdapAddress string `mapstructure:"ldap-address"` - LdapBindDN string `mapstructure:"ldap-bind-dn"` - LdapBindPassword string `mapstructure:"ldap-bind-password"` - LdapBaseDN string `mapstructure:"ldap-base-dn"` - LdapInsecure bool `mapstructure:"ldap-insecure"` - LdapSearchFilter string `mapstructure:"ldap-search-filter"` - ResourcesDir string `mapstructure:"resources-dir"` - DatabasePath string `mapstructure:"database-path" validate:"required"` - TrustedProxies string `mapstructure:"trusted-proxies"` + Port int `mapstructure:"port" validate:"required"` + Address string `validate:"required,ip4_addr" mapstructure:"address"` + AppURL string `validate:"required,url" mapstructure:"app-url"` + Users string `mapstructure:"users"` + UsersFile string `mapstructure:"users-file"` + SecureCookie bool `mapstructure:"secure-cookie"` + OAuthWhitelist string `mapstructure:"oauth-whitelist"` + OAuthAutoRedirect string `mapstructure:"oauth-auto-redirect"` + SessionExpiry int `mapstructure:"session-expiry"` + LogLevel string `mapstructure:"log-level" validate:"oneof=trace debug info warn error fatal panic"` + Title string `mapstructure:"app-title"` + LoginTimeout int `mapstructure:"login-timeout"` + LoginMaxRetries int `mapstructure:"login-max-retries"` + ForgotPasswordMessage string `mapstructure:"forgot-password-message"` + BackgroundImage string `mapstructure:"background-image" validate:"required"` + LdapAddress string `mapstructure:"ldap-address"` + LdapBindDN string `mapstructure:"ldap-bind-dn"` + LdapBindPassword string `mapstructure:"ldap-bind-password"` + LdapBaseDN string `mapstructure:"ldap-base-dn"` + LdapInsecure bool `mapstructure:"ldap-insecure"` + LdapSearchFilter string `mapstructure:"ldap-search-filter"` + ResourcesDir string `mapstructure:"resources-dir"` + DatabasePath string `mapstructure:"database-path" validate:"required"` + TrustedProxies string `mapstructure:"trusted-proxies"` } // OAuth/OIDC config @@ -66,14 +51,16 @@ type Claims struct { } type OAuthServiceConfig struct { - ClientID string - ClientSecret string - Scopes []string - RedirectURL string - AuthURL string - TokenURL string - UserinfoURL string - InsecureSkipVerify bool + ClientID string `key:"client-id"` + ClientSecret string `key:"client-secret"` + ClientSecretFile string `key:"client-secret-file"` + Scopes []string `key:"scopes"` + RedirectURL string `key:"redirect-url"` + AuthURL string `key:"auth-url"` + TokenURL string `key:"token-url"` + UserinfoURL string `key:"user-info-url"` + InsecureSkipVerify bool `key:"insecure-skip-verify"` + Name string `key:"name"` } // User/session related stuff @@ -97,6 +84,7 @@ type SessionCookie struct { Provider string TotpPending bool OAuthGroups string + OAuthName string } type UserContext struct { @@ -109,6 +97,7 @@ type UserContext struct { TotpPending bool OAuthGroups string TotpEnabled bool + OAuthName string } // API responses and queries @@ -174,3 +163,9 @@ type AppPath struct { Allow string Block string } + +// Flags + +type Providers struct { + Providers map[string]OAuthServiceConfig +} diff --git a/internal/controller/context_controller.go b/internal/controller/context_controller.go index ee3eec69..80ec61a7 100644 --- a/internal/controller/context_controller.go +++ b/internal/controller/context_controller.go @@ -19,25 +19,30 @@ type UserContextResponse struct { Provider string `json:"provider"` OAuth bool `json:"oauth"` TotpPending bool `json:"totpPending"` + OAuthName string `json:"oauthName"` } type AppContextResponse struct { - Status int `json:"status"` - Message string `json:"message"` - ConfiguredProviders []string `json:"configuredProviders"` - Title string `json:"title"` - GenericName string `json:"genericName"` - AppURL string `json:"appUrl"` - CookieDomain string `json:"cookieDomain"` - ForgotPasswordMessage string `json:"forgotPasswordMessage"` - BackgroundImage string `json:"backgroundImage"` - OAuthAutoRedirect string `json:"oauthAutoRedirect"` + Status int `json:"status"` + Message string `json:"message"` + Providers []Provider `json:"providers"` + Title string `json:"title"` + AppURL string `json:"appUrl"` + CookieDomain string `json:"cookieDomain"` + ForgotPasswordMessage string `json:"forgotPasswordMessage"` + BackgroundImage string `json:"backgroundImage"` + OAuthAutoRedirect string `json:"oauthAutoRedirect"` +} + +type Provider struct { + Name string `json:"name"` + ID string `json:"id"` + OAuth bool `json:"oauth"` } type ContextControllerConfig struct { - ConfiguredProviders []string + Providers []Provider Title string - GenericName string AppURL string CookieDomain string ForgotPasswordMessage string @@ -76,6 +81,7 @@ func (controller *ContextController) userContextHandler(c *gin.Context) { Provider: context.Provider, OAuth: context.OAuth, TotpPending: context.TotpPending, + OAuthName: context.OAuthName, } if err != nil { @@ -96,9 +102,8 @@ func (controller *ContextController) appContextHandler(c *gin.Context) { c.JSON(200, AppContextResponse{ Status: 200, Message: "Success", - ConfiguredProviders: controller.config.ConfiguredProviders, + Providers: controller.config.Providers, Title: controller.config.Title, - GenericName: controller.config.GenericName, AppURL: fmt.Sprintf("%s://%s", appUrl.Scheme, appUrl.Host), CookieDomain: controller.config.CookieDomain, ForgotPasswordMessage: controller.config.ForgotPasswordMessage, diff --git a/internal/controller/context_controller_test.go b/internal/controller/context_controller_test.go index 44f77a17..85be0b52 100644 --- a/internal/controller/context_controller_test.go +++ b/internal/controller/context_controller_test.go @@ -12,9 +12,19 @@ import ( ) var controllerCfg = controller.ContextControllerConfig{ - ConfiguredProviders: []string{"github", "google", "generic"}, + Providers: []controller.Provider{ + { + Name: "Username", + ID: "username", + OAuth: false, + }, + { + Name: "Google", + ID: "google", + OAuth: true, + }, + }, Title: "Test App", - GenericName: "Generic", AppURL: "http://localhost:8080", CookieDomain: "localhost", ForgotPasswordMessage: "Contact admin to reset your password.", @@ -58,9 +68,8 @@ func TestAppContextHandler(t *testing.T) { expectedRes := controller.AppContextResponse{ Status: 200, Message: "Success", - ConfiguredProviders: controllerCfg.ConfiguredProviders, + Providers: controllerCfg.Providers, Title: controllerCfg.Title, - GenericName: controllerCfg.GenericName, AppURL: controllerCfg.AppURL, CookieDomain: controllerCfg.CookieDomain, ForgotPasswordMessage: controllerCfg.ForgotPasswordMessage, diff --git a/internal/controller/oauth_controller.go b/internal/controller/oauth_controller.go index a65b53a0..bf50ff91 100644 --- a/internal/controller/oauth_controller.go +++ b/internal/controller/oauth_controller.go @@ -186,6 +186,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { Email: user.Email, Provider: req.Provider, OAuthGroups: utils.CoalesceToString(user.Groups), + OAuthName: service.GetName(), }) if err != nil { diff --git a/internal/middleware/context_middleware.go b/internal/middleware/context_middleware.go index 30fa6230..2c903be3 100644 --- a/internal/middleware/context_middleware.go +++ b/internal/middleware/context_middleware.go @@ -95,6 +95,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { Email: cookie.Email, Provider: cookie.Provider, OAuthGroups: cookie.OAuthGroups, + OAuthName: cookie.OAuthName, IsLoggedIn: true, OAuth: true, }) diff --git a/internal/model/session_model.go b/internal/model/session_model.go index 45e60659..0fdb6c37 100644 --- a/internal/model/session_model.go +++ b/internal/model/session_model.go @@ -9,4 +9,5 @@ type Session struct { TOTPPending bool `gorm:"column:totp_pending"` OAuthGroups string `gorm:"column:oauth_groups"` Expiry int64 `gorm:"column:expiry"` + OAuthName string `gorm:"column:oauth_name"` } diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index a3f8ed01..8925e491 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -210,6 +210,7 @@ func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.Sessio TOTPPending: data.TotpPending, OAuthGroups: data.OAuthGroups, Expiry: time.Now().Add(time.Duration(expiry) * time.Second).Unix(), + OAuthName: data.OAuthName, } err = auth.database.Create(&session).Error @@ -278,6 +279,7 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie, Provider: session.Provider, TotpPending: session.TOTPPending, OAuthGroups: session.OAuthGroups, + OAuthName: session.OAuthName, }, nil } diff --git a/internal/service/generic_oauth_service.go b/internal/service/generic_oauth_service.go index 72c23572..aae89c46 100644 --- a/internal/service/generic_oauth_service.go +++ b/internal/service/generic_oauth_service.go @@ -22,6 +22,7 @@ type GenericOAuthService struct { verifier string insecureSkipVerify bool userinfoUrl string + name string } func NewGenericOAuthService(config config.OAuthServiceConfig) *GenericOAuthService { @@ -38,6 +39,7 @@ func NewGenericOAuthService(config config.OAuthServiceConfig) *GenericOAuthServi }, insecureSkipVerify: config.InsecureSkipVerify, userinfoUrl: config.UserinfoURL, + name: config.Name, } } @@ -115,3 +117,7 @@ func (generic *GenericOAuthService) Userinfo() (config.Claims, error) { return user, nil } + +func (generic *GenericOAuthService) GetName() string { + return generic.name +} diff --git a/internal/service/github_oauth_service.go b/internal/service/github_oauth_service.go index 26d73b13..163c2c88 100644 --- a/internal/service/github_oauth_service.go +++ b/internal/service/github_oauth_service.go @@ -33,6 +33,7 @@ type GithubOAuthService struct { context context.Context token *oauth2.Token verifier string + name string } func NewGithubOAuthService(config config.OAuthServiceConfig) *GithubOAuthService { @@ -44,6 +45,7 @@ func NewGithubOAuthService(config config.OAuthServiceConfig) *GithubOAuthService Scopes: GithubOAuthScopes, Endpoint: endpoints.GitHub, }, + name: config.Name, } } @@ -167,3 +169,7 @@ func (github *GithubOAuthService) Userinfo() (config.Claims, error) { return user, nil } + +func (github *GithubOAuthService) GetName() string { + return github.name +} diff --git a/internal/service/google_oauth_service.go b/internal/service/google_oauth_service.go index 0f8c7eb8..ab0597d3 100644 --- a/internal/service/google_oauth_service.go +++ b/internal/service/google_oauth_service.go @@ -28,6 +28,7 @@ type GoogleOAuthService struct { context context.Context token *oauth2.Token verifier string + name string } func NewGoogleOAuthService(config config.OAuthServiceConfig) *GoogleOAuthService { @@ -39,6 +40,7 @@ func NewGoogleOAuthService(config config.OAuthServiceConfig) *GoogleOAuthService Scopes: GoogleOAuthScopes, Endpoint: endpoints.Google, }, + name: config.Name, } } @@ -111,3 +113,7 @@ func (google *GoogleOAuthService) Userinfo() (config.Claims, error) { return user, nil } + +func (google *GoogleOAuthService) GetName() string { + return google.name +} diff --git a/internal/service/oauth_broker_service.go b/internal/service/oauth_broker_service.go index f9df4f87..e6c6ddb3 100644 --- a/internal/service/oauth_broker_service.go +++ b/internal/service/oauth_broker_service.go @@ -14,6 +14,7 @@ type OAuthService interface { GetAuthURL(state string) string VerifyCode(code string) error Userinfo() (config.Claims, error) + GetName() string } type OAuthBrokerService struct { diff --git a/internal/utils/app_utils.go b/internal/utils/app_utils.go index c4b98c6b..643c9cf0 100644 --- a/internal/utils/app_utils.go +++ b/internal/utils/app_utils.go @@ -6,6 +6,9 @@ import ( "net/url" "strings" "tinyauth/internal/config" + "tinyauth/internal/utils/decoders" + + "maps" "github.com/gin-gonic/gin" "github.com/rs/zerolog" @@ -130,3 +133,68 @@ func GetLogLevel(level string) zerolog.Level { return zerolog.InfoLevel } } + +func GetOAuthProvidersConfig(env []string, args []string, appUrl string) (map[string]config.OAuthServiceConfig, error) { + providers := make(map[string]config.OAuthServiceConfig) + + // Get from environment variables + envMap := make(map[string]string) + + for _, e := range env { + pair := strings.SplitN(e, "=", 2) + if len(pair) == 2 { + envMap[pair[0]] = pair[1] + } + } + + envProviders, err := decoders.DecodeEnv(envMap) + + if err != nil { + return nil, err + } + + maps.Copy(providers, envProviders.Providers) + + // Get from flags + flagsMap := make(map[string]string) + + for _, arg := range args[1:] { + if strings.HasPrefix(arg, "--") { + pair := strings.SplitN(arg[2:], "=", 2) + if len(pair) == 2 { + flagsMap[pair[0]] = pair[1] + } + } + } + + flagProviders, err := decoders.DecodeFlags(flagsMap) + + if err != nil { + return nil, err + } + + maps.Copy(providers, flagProviders.Providers) + + // For every provider get correct secret from file if set + for name, provider := range providers { + secret := GetSecret(provider.ClientSecret, provider.ClientSecretFile) + provider.ClientSecret = secret + provider.ClientSecretFile = "" + providers[name] = provider + } + + // If we have google/github providers and no redirect URL babysit them + babysitProviders := []string{"google", "github"} + + for _, name := range babysitProviders { + if provider, exists := providers[name]; exists { + if provider.RedirectURL == "" { + provider.RedirectURL = appUrl + "/api/oauth/callback/" + name + providers[name] = provider + } + } + } + + // Return combined providers + return providers, nil +} diff --git a/internal/utils/app_utils_test.go b/internal/utils/app_utils_test.go index c35db3d7..a7f09fe6 100644 --- a/internal/utils/app_utils_test.go +++ b/internal/utils/app_utils_test.go @@ -1,6 +1,7 @@ package utils_test import ( + "os" "testing" "tinyauth/internal/config" "tinyauth/internal/utils" @@ -200,3 +201,71 @@ func TestIsRedirectSafe(t *testing.T) { result = utils.IsRedirectSafe(redirectURL, domain) assert.Equal(t, false, result) } + +func TestGetOAuthProvidersConfig(t *testing.T) { + env := []string{"PROVIDERS_CLIENT1_CLIENT_ID=client1-id", "PROVIDERS_CLIENT1_CLIENT_SECRET=client1-secret"} + args := []string{"/tinyauth/tinyauth", "--providers-client2-client-id=client2-id", "--providers-client2-client-secret=client2-secret"} + + expected := map[string]config.OAuthServiceConfig{ + "client1": { + ClientID: "client1-id", + ClientSecret: "client1-secret", + }, + "client2": { + ClientID: "client2-id", + ClientSecret: "client2-secret", + }, + } + + result, err := utils.GetOAuthProvidersConfig(env, args, "") + assert.NilError(t, err) + assert.DeepEqual(t, expected, result) + + // Case with no providers + env = []string{} + args = []string{"/tinyauth/tinyauth"} + expected = map[string]config.OAuthServiceConfig{} + + result, err = utils.GetOAuthProvidersConfig(env, args, "") + assert.NilError(t, err) + assert.DeepEqual(t, expected, result) + + // Case with secret from file + file, err := os.Create("/tmp/tinyauth_test_file") + assert.NilError(t, err) + + _, err = file.WriteString("file content\n") + assert.NilError(t, err) + + err = file.Close() + assert.NilError(t, err) + defer os.Remove("/tmp/tinyauth_test_file") + + env = []string{"PROVIDERS_CLIENT1_CLIENT_ID=client1-id", "PROVIDERS_CLIENT1_CLIENT_SECRET_FILE=/tmp/tinyauth_test_file"} + args = []string{"/tinyauth/tinyauth"} + expected = map[string]config.OAuthServiceConfig{ + "client1": { + ClientID: "client1-id", + ClientSecret: "file content", + }, + } + + result, err = utils.GetOAuthProvidersConfig(env, args, "") + assert.NilError(t, err) + assert.DeepEqual(t, expected, result) + + // Case with google provider and no redirect URL + env = []string{"PROVIDERS_GOOGLE_CLIENT_ID=google-id", "PROVIDERS_GOOGLE_CLIENT_SECRET=google-secret"} + args = []string{"/tinyauth/tinyauth"} + expected = map[string]config.OAuthServiceConfig{ + "google": { + ClientID: "google-id", + ClientSecret: "google-secret", + RedirectURL: "http://app.url/api/oauth/callback/google", + }, + } + + result, err = utils.GetOAuthProvidersConfig(env, args, "http://app.url") + assert.NilError(t, err) + assert.DeepEqual(t, expected, result) +} diff --git a/internal/utils/decoders/decoders.go b/internal/utils/decoders/decoders.go new file mode 100644 index 00000000..72a11d5b --- /dev/null +++ b/internal/utils/decoders/decoders.go @@ -0,0 +1,81 @@ +package decoders + +import ( + "reflect" + "strings" + "tinyauth/internal/config" +) + +func NormalizeKeys(keys map[string]string, rootName string, sep string) map[string]string { + normalized := make(map[string]string) + knownKeys := getKnownKeys() + + for k, v := range keys { + var finalKey []string + var suffix string + var camelClientName string + var camelField string + + finalKey = append(finalKey, rootName) + finalKey = append(finalKey, "providers") + cebabKey := strings.ToLower(k) + + for _, known := range knownKeys { + if strings.HasSuffix(cebabKey, strings.ReplaceAll(known, "-", sep)) { + suffix = known + break + } + } + + if suffix == "" { + continue + } + + clientNameParts := strings.Split(strings.TrimPrefix(strings.TrimSuffix(cebabKey, sep+strings.ReplaceAll(suffix, "-", sep)), "providers"+sep), sep) + + for i, p := range clientNameParts { + if i == 0 { + camelClientName += p + continue + } + if p == "" { + continue + } + camelClientName += strings.ToUpper(string([]rune(p)[0])) + string([]rune(p)[1:]) + } + + finalKey = append(finalKey, camelClientName) + + filedParts := strings.Split(suffix, "-") + + for i, p := range filedParts { + if i == 0 { + camelField += p + continue + } + if p == "" { + continue + } + camelField += strings.ToUpper(string([]rune(p)[0])) + string([]rune(p)[1:]) + } + + finalKey = append(finalKey, camelField) + normalized[strings.Join(finalKey, ".")] = v + } + + return normalized +} + +func getKnownKeys() []string { + var known []string + + p := config.OAuthServiceConfig{} + v := reflect.ValueOf(p) + typeOfP := v.Type() + + for field := range typeOfP.NumField() { + known = append(known, typeOfP.Field(field).Tag.Get("key")) + } + + return known +} diff --git a/internal/utils/decoders/decoders_test.go b/internal/utils/decoders/decoders_test.go new file mode 100644 index 00000000..285760c9 --- /dev/null +++ b/internal/utils/decoders/decoders_test.go @@ -0,0 +1,44 @@ +package decoders_test + +import ( + "testing" + "tinyauth/internal/utils/decoders" + + "gotest.tools/v3/assert" +) + +func TestNormalizeKeys(t *testing.T) { + // Test with env + test := map[string]string{ + "PROVIDERS_CLIENT1_CLIENT_ID": "my-client-id", + "PROVIDERS_CLIENT1_CLIENT_SECRET": "my-client-secret", + "PROVIDERS_MY_AWESOME_CLIENT_CLIENT_ID": "my-awesome-client-id", + "PROVIDERS_MY_AWESOME_CLIENT_CLIENT_SECRET_FILE": "/path/to/secret", + } + expected := map[string]string{ + "tinyauth.providers.client1.clientId": "my-client-id", + "tinyauth.providers.client1.clientSecret": "my-client-secret", + "tinyauth.providers.myAwesomeClient.clientId": "my-awesome-client-id", + "tinyauth.providers.myAwesomeClient.clientSecretFile": "/path/to/secret", + } + + normalized := decoders.NormalizeKeys(test, "tinyauth", "_") + assert.DeepEqual(t, normalized, expected) + + // Test with flags (assume -- is already stripped) + test = map[string]string{ + "providers-client1-client-id": "my-client-id", + "providers-client1-client-secret": "my-client-secret", + "providers-my-awesome-client-client-id": "my-awesome-client-id", + "providers-my-awesome-client-client-secret-file": "/path/to/secret", + } + expected = map[string]string{ + "tinyauth.providers.client1.clientId": "my-client-id", + "tinyauth.providers.client1.clientSecret": "my-client-secret", + "tinyauth.providers.myAwesomeClient.clientId": "my-awesome-client-id", + "tinyauth.providers.myAwesomeClient.clientSecretFile": "/path/to/secret", + } + + normalized = decoders.NormalizeKeys(test, "tinyauth", "-") + assert.DeepEqual(t, normalized, expected) +} diff --git a/internal/utils/decoders/env_decoder.go b/internal/utils/decoders/env_decoder.go new file mode 100644 index 00000000..4164aa55 --- /dev/null +++ b/internal/utils/decoders/env_decoder.go @@ -0,0 +1,20 @@ +package decoders + +import ( + "tinyauth/internal/config" + + "github.com/traefik/paerser/parser" +) + +func DecodeEnv(env map[string]string) (config.Providers, error) { + normalized := NormalizeKeys(env, "tinyauth", "_") + var providers config.Providers + + err := parser.Decode(normalized, &providers, "tinyauth", "tinyauth.providers") + + if err != nil { + return config.Providers{}, err + } + + return providers, nil +} diff --git a/internal/utils/decoders/env_decoder_test.go b/internal/utils/decoders/env_decoder_test.go new file mode 100644 index 00000000..2233241f --- /dev/null +++ b/internal/utils/decoders/env_decoder_test.go @@ -0,0 +1,60 @@ +package decoders_test + +import ( + "testing" + "tinyauth/internal/config" + "tinyauth/internal/utils/decoders" + + "gotest.tools/v3/assert" +) + +func TestDecodeEnv(t *testing.T) { + // Variables + expected := config.Providers{ + Providers: map[string]config.OAuthServiceConfig{ + "client1": { + ClientID: "client1-id", + ClientSecret: "client1-secret", + Scopes: []string{"client1-scope1", "client1-scope2"}, + RedirectURL: "client1-redirect-url", + AuthURL: "client1-auth-url", + UserinfoURL: "client1-user-info-url", + Name: "Client1", + InsecureSkipVerify: false, + }, + "client2": { + ClientID: "client2-id", + ClientSecret: "client2-secret", + Scopes: []string{"client2-scope1", "client2-scope2"}, + RedirectURL: "client2-redirect-url", + AuthURL: "client2-auth-url", + UserinfoURL: "client2-user-info-url", + Name: "My Awesome Client2", + InsecureSkipVerify: false, + }, + }, + } + test := map[string]string{ + "PROVIDERS_CLIENT1_CLIENT_ID": "client1-id", + "PROVIDERS_CLIENT1_CLIENT_SECRET": "client1-secret", + "PROVIDERS_CLIENT1_SCOPES": "client1-scope1,client1-scope2", + "PROVIDERS_CLIENT1_REDIRECT_URL": "client1-redirect-url", + "PROVIDERS_CLIENT1_AUTH_URL": "client1-auth-url", + "PROVIDERS_CLIENT1_USER_INFO_URL": "client1-user-info-url", + "PROVIDERS_CLIENT1_NAME": "Client1", + "PROVIDERS_CLIENT1_INSECURE_SKIP_VERIFY": "false", + "PROVIDERS_CLIENT2_CLIENT_ID": "client2-id", + "PROVIDERS_CLIENT2_CLIENT_SECRET": "client2-secret", + "PROVIDERS_CLIENT2_SCOPES": "client2-scope1,client2-scope2", + "PROVIDERS_CLIENT2_REDIRECT_URL": "client2-redirect-url", + "PROVIDERS_CLIENT2_AUTH_URL": "client2-auth-url", + "PROVIDERS_CLIENT2_USER_INFO_URL": "client2-user-info-url", + "PROVIDERS_CLIENT2_NAME": "My Awesome Client2", + "PROVIDERS_CLIENT2_INSECURE_SKIP_VERIFY": "false", + } + + // Test + res, err := decoders.DecodeEnv(test) + assert.NilError(t, err) + assert.DeepEqual(t, expected, res) +} diff --git a/internal/utils/decoders/flags_decoder.go b/internal/utils/decoders/flags_decoder.go new file mode 100644 index 00000000..d973d299 --- /dev/null +++ b/internal/utils/decoders/flags_decoder.go @@ -0,0 +1,30 @@ +package decoders + +import ( + "strings" + "tinyauth/internal/config" + + "github.com/traefik/paerser/parser" +) + +func DecodeFlags(flags map[string]string) (config.Providers, error) { + filtered := filterFlags(flags) + normalized := NormalizeKeys(filtered, "tinyauth", "-") + var providers config.Providers + + err := parser.Decode(normalized, &providers, "tinyauth", "tinyauth.providers") + + if err != nil { + return config.Providers{}, err + } + + return providers, nil +} + +func filterFlags(flags map[string]string) map[string]string { + filtered := make(map[string]string) + for k, v := range flags { + filtered[strings.TrimPrefix(k, "--")] = v + } + return filtered +} diff --git a/internal/utils/decoders/flags_decoder_test.go b/internal/utils/decoders/flags_decoder_test.go new file mode 100644 index 00000000..356b4ae7 --- /dev/null +++ b/internal/utils/decoders/flags_decoder_test.go @@ -0,0 +1,60 @@ +package decoders_test + +import ( + "testing" + "tinyauth/internal/config" + "tinyauth/internal/utils/decoders" + + "gotest.tools/v3/assert" +) + +func TestDecodeFlags(t *testing.T) { + // Variables + expected := config.Providers{ + Providers: map[string]config.OAuthServiceConfig{ + "client1": { + ClientID: "client1-id", + ClientSecret: "client1-secret", + Scopes: []string{"client1-scope1", "client1-scope2"}, + RedirectURL: "client1-redirect-url", + AuthURL: "client1-auth-url", + UserinfoURL: "client1-user-info-url", + Name: "Client1", + InsecureSkipVerify: false, + }, + "client2": { + ClientID: "client2-id", + ClientSecret: "client2-secret", + Scopes: []string{"client2-scope1", "client2-scope2"}, + RedirectURL: "client2-redirect-url", + AuthURL: "client2-auth-url", + UserinfoURL: "client2-user-info-url", + Name: "My Awesome Client2", + InsecureSkipVerify: false, + }, + }, + } + test := map[string]string{ + "--providers-client1-client-id": "client1-id", + "--providers-client1-client-secret": "client1-secret", + "--providers-client1-scopes": "client1-scope1,client1-scope2", + "--providers-client1-redirect-url": "client1-redirect-url", + "--providers-client1-auth-url": "client1-auth-url", + "--providers-client1-user-info-url": "client1-user-info-url", + "--providers-client1-name": "Client1", + "--providers-client1-insecure-skip-verify": "false", + "--providers-client2-client-id": "client2-id", + "--providers-client2-client-secret": "client2-secret", + "--providers-client2-scopes": "client2-scope1,client2-scope2", + "--providers-client2-redirect-url": "client2-redirect-url", + "--providers-client2-auth-url": "client2-auth-url", + "--providers-client2-user-info-url": "client2-user-info-url", + "--providers-client2-name": "My Awesome Client2", + "--providers-client2-insecure-skip-verify": "false", + } + + // Test + res, err := decoders.DecodeFlags(test) + assert.NilError(t, err) + assert.DeepEqual(t, expected, res) +} diff --git a/internal/utils/decoders/label_decoder_test.go b/internal/utils/decoders/label_decoder_test.go index 1df885cd..63189d19 100644 --- a/internal/utils/decoders/label_decoder_test.go +++ b/internal/utils/decoders/label_decoder_test.go @@ -1,10 +1,11 @@ package decoders_test import ( - "reflect" "testing" "tinyauth/internal/config" "tinyauth/internal/utils/decoders" + + "gotest.tools/v3/assert" ) func TestDecodeLabels(t *testing.T) { @@ -62,12 +63,6 @@ func TestDecodeLabels(t *testing.T) { // Test result, err := decoders.DecodeLabels(test) - - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - - if reflect.DeepEqual(expected, result) == false { - t.Fatalf("Expected %v but got %v", expected, result) - } + assert.NilError(t, err) + assert.DeepEqual(t, expected, result) } From 039bdb4785c5f76d754297ffe67e28cd794bddfe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Sep 2025 13:29:10 +0300 Subject: [PATCH 026/127] chore(deps): bump the minor-patch group across 1 directory with 6 updates (#351) Bumps the minor-patch group with 6 updates in the / directory: | Package | From | To | | --- | --- | --- | | [github.com/golang-migrate/migrate/v4](https://github.com/golang-migrate/migrate) | `4.18.3` | `4.19.0` | | [github.com/spf13/cobra](https://github.com/spf13/cobra) | `1.9.1` | `1.10.1` | | [github.com/spf13/viper](https://github.com/spf13/viper) | `1.20.1` | `1.21.0` | | [gorm.io/gorm](https://github.com/go-gorm/gorm) | `1.30.1` | `1.30.5` | | [github.com/docker/docker](https://github.com/docker/docker) | `28.3.3+incompatible` | `28.4.0+incompatible` | | [golang.org/x/oauth2](https://github.com/golang/oauth2) | `0.30.0` | `0.31.0` | Updates `github.com/golang-migrate/migrate/v4` from 4.18.3 to 4.19.0 - [Release notes](https://github.com/golang-migrate/migrate/releases) - [Changelog](https://github.com/golang-migrate/migrate/blob/master/.goreleaser.yml) - [Commits](https://github.com/golang-migrate/migrate/compare/v4.18.3...v4.19.0) Updates `github.com/spf13/cobra` from 1.9.1 to 1.10.1 - [Release notes](https://github.com/spf13/cobra/releases) - [Commits](https://github.com/spf13/cobra/compare/v1.9.1...v1.10.1) Updates `github.com/spf13/viper` from 1.20.1 to 1.21.0 - [Release notes](https://github.com/spf13/viper/releases) - [Commits](https://github.com/spf13/viper/compare/v1.20.1...v1.21.0) Updates `gorm.io/gorm` from 1.30.1 to 1.30.5 - [Release notes](https://github.com/go-gorm/gorm/releases) - [Commits](https://github.com/go-gorm/gorm/compare/v1.30.1...v1.30.5) Updates `github.com/docker/docker` from 28.3.3+incompatible to 28.4.0+incompatible - [Release notes](https://github.com/docker/docker/releases) - [Commits](https://github.com/docker/docker/compare/v28.3.3...v28.4.0) Updates `golang.org/x/oauth2` from 0.30.0 to 0.31.0 - [Commits](https://github.com/golang/oauth2/compare/v0.30.0...v0.31.0) --- updated-dependencies: - dependency-name: github.com/golang-migrate/migrate/v4 dependency-version: 4.19.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: github.com/spf13/cobra dependency-version: 1.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: github.com/spf13/viper dependency-version: 1.21.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: gorm.io/gorm dependency-version: 1.30.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: github.com/docker/docker dependency-version: 28.4.0+incompatible dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: golang.org/x/oauth2 dependency-version: 0.31.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 40 ++++++++++--------- go.sum | 118 +++++++++++++++++++++------------------------------------ 2 files changed, 63 insertions(+), 95 deletions(-) diff --git a/go.mod b/go.mod index 41b9c263..7d2e7b9a 100644 --- a/go.mod +++ b/go.mod @@ -9,18 +9,18 @@ require ( github.com/gin-gonic/gin v1.10.1 github.com/glebarez/sqlite v1.11.0 github.com/go-playground/validator/v10 v10.27.0 - github.com/golang-migrate/migrate/v4 v4.18.3 + github.com/golang-migrate/migrate/v4 v4.19.0 github.com/google/go-querystring v1.1.0 github.com/google/uuid v1.6.0 github.com/mdp/qrterminal/v3 v3.2.1 github.com/rs/zerolog v1.34.0 - github.com/spf13/cobra v1.9.1 - github.com/spf13/viper v1.20.1 + github.com/spf13/cobra v1.10.1 + github.com/spf13/viper v1.21.0 github.com/traefik/paerser v0.2.2 github.com/weppos/publicsuffix-go v0.50.0 golang.org/x/crypto v0.42.0 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b - gorm.io/gorm v1.30.1 + gorm.io/gorm v1.30.5 gotest.tools/v3 v3.5.2 ) @@ -34,7 +34,7 @@ require ( github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect @@ -48,6 +48,7 @@ require ( go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect go.opentelemetry.io/otel/sdk v1.34.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/term v0.35.0 // indirect modernc.org/libc v1.66.3 // indirect modernc.org/mathutil v1.7.1 // indirect @@ -73,22 +74,21 @@ require ( github.com/charmbracelet/x/term v0.2.1 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/docker v28.3.3+incompatible + github.com/docker/docker v28.4.0+incompatible 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/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gin-contrib/sse v1.0.0 // indirect github.com/go-ldap/ldap/v3 v3.4.11 - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/goccy/go-json v0.10.4 // indirect - github.com/gogo/protobuf v1.3.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect @@ -107,27 +107,25 @@ require ( github.com/muesli/termenv v0.16.0 // 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.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pquerna/otp v1.5.0 github.com/rivo/uniseg v0.4.7 // indirect - github.com/sagikazarmark/locafero v0.7.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.12.0 // indirect - github.com/spf13/cast v1.7.1 // indirect - github.com/spf13/pflag v1.0.6 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect - go.opentelemetry.io/otel v1.34.0 // indirect - go.opentelemetry.io/otel/metric v1.34.0 // indirect - go.opentelemetry.io/otel/trace v1.34.0 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.9.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect golang.org/x/arch v0.13.0 // indirect golang.org/x/net v0.44.0 // indirect - golang.org/x/oauth2 v0.30.0 + golang.org/x/oauth2 v0.31.0 golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.36.0 // indirect golang.org/x/text v0.29.0 // indirect diff --git a/go.sum b/go.sum index 5d1645b8..8147ebe4 100644 --- a/go.sum +++ b/go.sum @@ -74,8 +74,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/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 v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= -github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk= +github.com/docker/docker v28.4.0+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= @@ -88,8 +88,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 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/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= @@ -105,8 +105,8 @@ github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:h github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU= github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/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= @@ -122,13 +122,11 @@ github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlnd 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/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/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs= -github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= +github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE= +github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -165,8 +163,6 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 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/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.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= @@ -225,8 +221,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 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/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -244,22 +240,23 @@ github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= -github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= -github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= -github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= -github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= -github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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= @@ -270,8 +267,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/traefik/paerser v0.2.2 h1:cpzW/ZrQrBh3mdwD/jnp6aXASiUFKOVr6ldP+keJTcQ= @@ -284,59 +282,40 @@ github.com/weppos/publicsuffix-go v0.50.0 h1:M178k6l8cnh9T1c1cStkhytVxdk5zPd6gGZ github.com/weppos/publicsuffix-go v0.50.0/go.mod h1:VXhClBYMlDrUsome4pOTpe68Ui0p6iQRAbyHQD1yKoU= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -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/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -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.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA= 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.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= -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/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= -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.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -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/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= +golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -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-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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -345,23 +324,14 @@ golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= -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.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= -golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= -golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-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/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= -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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= +google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= @@ -376,8 +346,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4= -gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +gorm.io/gorm v1.30.5 h1:dvEfYwxL+i+xgCNSGGBT1lDjCzfELK8fHZxL3Ee9X0s= +gorm.io/gorm v1.30.5/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= From 9a306f57ec24bc110560a616828e76907a22ce84 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Sep 2025 13:29:31 +0300 Subject: [PATCH 027/127] chore(deps): bump oven/bun from 1.2.21-alpine to 1.2.22-alpine (#358) Bumps oven/bun from 1.2.21-alpine to 1.2.22-alpine. --- updated-dependencies: - dependency-name: oven/bun dependency-version: 1.2.22-alpine dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 865dfe5e..55d8fa11 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Site builder -FROM oven/bun:1.2.21-alpine AS frontend-builder +FROM oven/bun:1.2.22-alpine AS frontend-builder WORKDIR /frontend From 363f0f932f8fbffaa05f987d2e1b55e97f5c3702 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Sep 2025 13:30:11 +0300 Subject: [PATCH 028/127] chore(deps): bump the minor-patch group in /frontend with 21 updates (#359) Bumps the minor-patch group in /frontend with 21 updates: | Package | From | To | | --- | --- | --- | | [@hookform/resolvers](https://github.com/react-hook-form/resolvers) | `5.2.1` | `5.2.2` | | [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.1.12` | `4.1.13` | | [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.85.5` | `5.87.4` | | [axios](https://github.com/axios/axios) | `1.11.0` | `1.12.2` | | [i18next](https://github.com/i18next/i18next) | `25.4.2` | `25.5.2` | | [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.541.0` | `0.544.0` | | [react-i18next](https://github.com/i18next/react-i18next) | `15.7.2` | `15.7.3` | | [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) | `7.8.2` | `7.9.1` | | [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.1.12` | `4.1.13` | | [zod](https://github.com/colinhacks/zod) | `4.1.3` | `4.1.8` | | [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) | `9.34.0` | `9.35.0` | | [@tanstack/eslint-plugin-query](https://github.com/TanStack/query/tree/HEAD/packages/eslint-plugin-query) | `5.83.1` | `5.86.0` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `24.3.0` | `24.4.0` | | [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.1.11` | `19.1.13` | | [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `19.1.8` | `19.1.9` | | [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `5.0.1` | `5.0.2` | | [eslint](https://github.com/eslint/eslint) | `9.34.0` | `9.35.0` | | [globals](https://github.com/sindresorhus/globals) | `16.3.0` | `16.4.0` | | [tw-animate-css](https://github.com/Wombosvideo/tw-animate-css) | `1.3.7` | `1.3.8` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.41.0` | `8.43.0` | | [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.1.3` | `7.1.5` | Updates `@hookform/resolvers` from 5.2.1 to 5.2.2 - [Release notes](https://github.com/react-hook-form/resolvers/releases) - [Commits](https://github.com/react-hook-form/resolvers/compare/v5.2.1...v5.2.2) Updates `@tailwindcss/vite` from 4.1.12 to 4.1.13 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.13/packages/@tailwindcss-vite) Updates `@tanstack/react-query` from 5.85.5 to 5.87.4 - [Release notes](https://github.com/TanStack/query/releases) - [Commits](https://github.com/TanStack/query/commits/v5.87.4/packages/react-query) Updates `axios` from 1.11.0 to 1.12.2 - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.11.0...v1.12.2) Updates `i18next` from 25.4.2 to 25.5.2 - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/i18next/compare/v25.4.2...v25.5.2) Updates `lucide-react` from 0.541.0 to 0.544.0 - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/0.544.0/packages/lucide-react) Updates `react-i18next` from 15.7.2 to 15.7.3 - [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/react-i18next/compare/v15.7.2...v15.7.3) Updates `react-router` from 7.8.2 to 7.9.1 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router@7.9.1/packages/react-router) Updates `tailwindcss` from 4.1.12 to 4.1.13 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.13/packages/tailwindcss) Updates `zod` from 4.1.3 to 4.1.8 - [Release notes](https://github.com/colinhacks/zod/releases) - [Commits](https://github.com/colinhacks/zod/compare/v4.1.3...v4.1.8) Updates `@eslint/js` from 9.34.0 to 9.35.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/commits/v9.35.0/packages/js) Updates `@tanstack/eslint-plugin-query` from 5.83.1 to 5.86.0 - [Release notes](https://github.com/TanStack/query/releases) - [Commits](https://github.com/TanStack/query/commits/v5.86.0/packages/eslint-plugin-query) Updates `@types/node` from 24.3.0 to 24.4.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/react` from 19.1.11 to 19.1.13 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react) Updates `@types/react-dom` from 19.1.8 to 19.1.9 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom) Updates `@vitejs/plugin-react` from 5.0.1 to 5.0.2 - [Release notes](https://github.com/vitejs/vite-plugin-react/releases) - [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@5.0.2/packages/plugin-react) Updates `eslint` from 9.34.0 to 9.35.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v9.34.0...v9.35.0) Updates `globals` from 16.3.0 to 16.4.0 - [Release notes](https://github.com/sindresorhus/globals/releases) - [Commits](https://github.com/sindresorhus/globals/compare/v16.3.0...v16.4.0) Updates `tw-animate-css` from 1.3.7 to 1.3.8 - [Release notes](https://github.com/Wombosvideo/tw-animate-css/releases) - [Commits](https://github.com/Wombosvideo/tw-animate-css/compare/v1.3.7...v1.3.8) Updates `typescript-eslint` from 8.41.0 to 8.43.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.43.0/packages/typescript-eslint) Updates `vite` from 7.1.3 to 7.1.5 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v7.1.5/packages/vite) --- updated-dependencies: - dependency-name: "@hookform/resolvers" dependency-version: 5.2.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: "@tailwindcss/vite" dependency-version: 4.1.13 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: "@tanstack/react-query" dependency-version: 5.87.4 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: axios dependency-version: 1.12.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: i18next dependency-version: 25.5.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: lucide-react dependency-version: 0.544.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: react-i18next dependency-version: 15.7.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: react-router dependency-version: 7.9.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: tailwindcss dependency-version: 4.1.13 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: zod dependency-version: 4.1.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: "@eslint/js" dependency-version: 9.35.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@tanstack/eslint-plugin-query" dependency-version: 5.86.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@types/node" dependency-version: 24.4.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@types/react" dependency-version: 19.1.13 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: "@types/react-dom" dependency-version: 19.1.9 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: "@vitejs/plugin-react" dependency-version: 5.0.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: eslint dependency-version: 9.35.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: globals dependency-version: 16.4.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: tw-animate-css dependency-version: 1.3.8 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: typescript-eslint dependency-version: 8.43.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: vite dependency-version: 7.1.5 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/bun.lock | 202 ++++++++++++++++++++++-------------------- frontend/package.json | 42 ++++----- 2 files changed, 128 insertions(+), 116 deletions(-) diff --git a/frontend/bun.lock b/frontend/bun.lock index 1f98f9c4..037318cd 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -4,49 +4,49 @@ "": { "name": "tinyauth-shadcn", "dependencies": { - "@hookform/resolvers": "^5.2.1", + "@hookform/resolvers": "^5.2.2", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", - "@tailwindcss/vite": "^4.1.12", - "@tanstack/react-query": "^5.85.5", - "axios": "^1.11.0", + "@tailwindcss/vite": "^4.1.13", + "@tanstack/react-query": "^5.87.4", + "axios": "^1.12.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "i18next": "^25.4.2", + "i18next": "^25.5.2", "i18next-browser-languagedetector": "^8.2.0", "i18next-resources-to-backend": "^1.2.1", "input-otp": "^1.4.2", - "lucide-react": "^0.541.0", + "lucide-react": "^0.544.0", "next-themes": "^0.4.6", "react": "^19.1.1", "react-dom": "^19.1.1", "react-hook-form": "^7.62.0", - "react-i18next": "^15.7.2", + "react-i18next": "^15.7.3", "react-markdown": "^10.1.0", - "react-router": "^7.8.2", + "react-router": "^7.9.1", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", - "tailwindcss": "^4.1.12", - "zod": "^4.1.3", + "tailwindcss": "^4.1.13", + "zod": "^4.1.8", }, "devDependencies": { - "@eslint/js": "^9.34.0", - "@tanstack/eslint-plugin-query": "^5.83.1", - "@types/node": "^24.3.0", - "@types/react": "^19.1.11", - "@types/react-dom": "^19.1.8", - "@vitejs/plugin-react": "^5.0.1", - "eslint": "^9.34.0", + "@eslint/js": "^9.35.0", + "@tanstack/eslint-plugin-query": "^5.86.0", + "@types/node": "^24.4.0", + "@types/react": "^19.1.13", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.2", + "eslint": "^9.35.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", - "globals": "^16.3.0", + "globals": "^16.4.0", "prettier": "3.6.2", - "tw-animate-css": "^1.3.7", + "tw-animate-css": "^1.3.8", "typescript": "~5.9.2", - "typescript-eslint": "^8.41.0", - "vite": "^7.1.3", + "typescript-eslint": "^8.43.0", + "vite": "^7.1.5", }, }, }, @@ -143,7 +143,7 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="], - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="], + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], @@ -155,7 +155,7 @@ "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], - "@eslint/js": ["@eslint/js@9.34.0", "", {}, "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw=="], + "@eslint/js": ["@eslint/js@9.35.0", "", {}, "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw=="], "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], @@ -169,7 +169,7 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="], - "@hookform/resolvers": ["@hookform/resolvers@5.2.1", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-u0+6X58gkjMcxur1wRWokA7XsiiBJ6aK17aPZxhkoYiK5J+HcTx0Vhu9ovXe6H+dVpO6cjrn2FkJTryXEMlryQ=="], + "@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], @@ -189,7 +189,7 @@ "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], @@ -255,7 +255,7 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.32", "", {}, "sha512-QReCdvxiUZAPkvp1xpAg62IeNzykOFA6syH2CnClif4YmALN1XKpB39XneL80008UbtMShthSVDKmrx05N1q/g=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.34", "", {}, "sha512-LyAREkZHP5pMom7c24meKmJCdhf2hEyvam2q0unr3or9ydwDL+DJ8chTF6Av/RFPb3rH8UFBdMzO5MxTZW97oA=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.46.2", "", { "os": "android", "cpu": "arm" }, "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA=="], @@ -299,41 +299,41 @@ "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], - "@tailwindcss/node": ["@tailwindcss/node@4.1.12", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.5.1", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.12" } }, "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ=="], + "@tailwindcss/node": ["@tailwindcss/node@4.1.13", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.5.1", "lightningcss": "1.30.1", "magic-string": "^0.30.18", "source-map-js": "^1.2.1", "tailwindcss": "4.1.13" } }, "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw=="], - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.12", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.12", "@tailwindcss/oxide-darwin-arm64": "4.1.12", "@tailwindcss/oxide-darwin-x64": "4.1.12", "@tailwindcss/oxide-freebsd-x64": "4.1.12", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.12", "@tailwindcss/oxide-linux-arm64-musl": "4.1.12", "@tailwindcss/oxide-linux-x64-gnu": "4.1.12", "@tailwindcss/oxide-linux-x64-musl": "4.1.12", "@tailwindcss/oxide-wasm32-wasi": "4.1.12", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.12", "@tailwindcss/oxide-win32-x64-msvc": "4.1.12" } }, "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw=="], + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.13", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.13", "@tailwindcss/oxide-darwin-arm64": "4.1.13", "@tailwindcss/oxide-darwin-x64": "4.1.13", "@tailwindcss/oxide-freebsd-x64": "4.1.13", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13", "@tailwindcss/oxide-linux-arm64-musl": "4.1.13", "@tailwindcss/oxide-linux-x64-gnu": "4.1.13", "@tailwindcss/oxide-linux-x64-musl": "4.1.13", "@tailwindcss/oxide-wasm32-wasi": "4.1.13", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13", "@tailwindcss/oxide-win32-x64-msvc": "4.1.13" } }, "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA=="], - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.12", "", { "os": "android", "cpu": "arm64" }, "sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ=="], + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.13", "", { "os": "android", "cpu": "arm64" }, "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew=="], - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw=="], + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ=="], - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg=="], + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw=="], - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww=="], + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.13", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ=="], - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.12", "", { "os": "linux", "cpu": "arm" }, "sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ=="], + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13", "", { "os": "linux", "cpu": "arm" }, "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw=="], - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g=="], + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ=="], - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA=="], + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg=="], - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.12", "", { "os": "linux", "cpu": "x64" }, "sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q=="], + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.13", "", { "os": "linux", "cpu": "x64" }, "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ=="], - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.12", "", { "os": "linux", "cpu": "x64" }, "sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A=="], + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.13", "", { "os": "linux", "cpu": "x64" }, "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ=="], - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.12", "", { "dependencies": { "@emnapi/core": "^1.4.5", "@emnapi/runtime": "^1.4.5", "@emnapi/wasi-threads": "^1.0.4", "@napi-rs/wasm-runtime": "^0.2.12", "@tybys/wasm-util": "^0.10.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg=="], + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.13", "", { "dependencies": { "@emnapi/core": "^1.4.5", "@emnapi/runtime": "^1.4.5", "@emnapi/wasi-threads": "^1.0.4", "@napi-rs/wasm-runtime": "^0.2.12", "@tybys/wasm-util": "^0.10.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA=="], - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg=="], + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg=="], - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.12", "", { "os": "win32", "cpu": "x64" }, "sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA=="], + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.13", "", { "os": "win32", "cpu": "x64" }, "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw=="], - "@tailwindcss/vite": ["@tailwindcss/vite@4.1.12", "", { "dependencies": { "@tailwindcss/node": "4.1.12", "@tailwindcss/oxide": "4.1.12", "tailwindcss": "4.1.12" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-4pt0AMFDx7gzIrAOIYgYP0KCBuKWqyW8ayrdiLEjoJTT4pKTjrzG/e4uzWtTLDziC+66R9wbUqZBccJalSE5vQ=="], + "@tailwindcss/vite": ["@tailwindcss/vite@4.1.13", "", { "dependencies": { "@tailwindcss/node": "4.1.13", "@tailwindcss/oxide": "4.1.13", "tailwindcss": "4.1.13" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ=="], - "@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.83.1", "", { "dependencies": { "@typescript-eslint/utils": "^8.37.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-tdkpPFfzkTksN9BIlT/qjixSAtKrsW6PUVRwdKWaOcag7DrD1vpki3UzzdfMQGDRGeg1Ue1Dg+rcl5FJGembNg=="], + "@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.86.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.37.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-tmXdnx/fF3yY5G5jpzrJQbASY3PNzsKF0gq9IsZVqz3LJ4sExgdUFGQ305nao0wTMBOclyrSC13v/VQ3yOXu/Q=="], - "@tanstack/query-core": ["@tanstack/query-core@5.85.5", "", {}, "sha512-KO0WTob4JEApv69iYp1eGvfMSUkgw//IpMnq+//cORBzXf0smyRwPLrUvEe5qtAEGjwZTXrjxg+oJNP/C00t6w=="], + "@tanstack/query-core": ["@tanstack/query-core@5.87.4", "", {}, "sha512-uNsg6zMxraEPDVO2Bn+F3/ctHi+Zsk+MMpcN8h6P7ozqD088F6mFY5TfGM7zuyIrL7HKpDyu6QHfLWiDxh3cuw=="], - "@tanstack/react-query": ["@tanstack/react-query@5.85.5", "", { "dependencies": { "@tanstack/query-core": "5.85.5" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-/X4EFNcnPiSs8wM2v+b6DqS5mmGeuJQvxBglmDxl6ZQb5V26ouD2SJYAcC3VjbNwqhY2zjxVD15rDA5nGbMn3A=="], + "@tanstack/react-query": ["@tanstack/react-query@5.87.4", "", { "dependencies": { "@tanstack/query-core": "5.87.4" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-T5GT/1ZaNsUXf5I3RhcYuT17I4CPlbZgyLxc/ZGv7ciS6esytlbjb3DgUFO6c8JWYMDpdjSWInyGZUErgzqhcA=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -357,37 +357,37 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + "@types/node": ["@types/node@24.4.0", "", { "dependencies": { "undici-types": "~7.11.0" } }, "sha512-gUuVEAK4/u6F9wRLznPUU4WGUacSEBDPoC2TrBkw3GAnOLHBL45QdfHOXp1kJ4ypBGLxTOB+t7NJLpKoC3gznQ=="], - "@types/react": ["@types/react@19.1.11", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ=="], + "@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="], - "@types/react-dom": ["@types/react-dom@19.1.8", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-xG7xaBMJCpcK0RpN8jDbAACQo54ycO6h4dSSmgv8+fu6ZIAdANkx/WsawASUjVXYfy+J9AbUpRMNNEsXCDfDBQ=="], + "@types/react-dom": ["@types/react-dom@19.1.9", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.41.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/type-utils": "8.41.0", "@typescript-eslint/utils": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.41.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-8fz6oa6wEKZrhXWro/S3n2eRJqlRcIa6SlDh59FXJ5Wp5XRZ8B9ixpJDcjadHq47hMx0u+HW6SNa6LjJQ6NLtw=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.43.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/type-utils": "8.43.0", "@typescript-eslint/utils": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.43.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.41.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/types": "8.41.0", "@typescript-eslint/typescript-estree": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.43.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/types": "8.43.0", "@typescript-eslint/typescript-estree": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.41.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.41.0", "@typescript-eslint/types": "^8.41.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.43.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.43.0", "@typescript-eslint/types": "^8.43.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0" } }, "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.41.0", "", { "dependencies": { "@typescript-eslint/types": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0" } }, "sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.41.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.43.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.41.0", "", { "dependencies": { "@typescript-eslint/types": "8.41.0", "@typescript-eslint/typescript-estree": "8.41.0", "@typescript-eslint/utils": "8.41.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-63qt1h91vg3KsjVVonFJWjgSK7pZHSQFKH6uwqxAH9bBrsyRhO6ONoKyXxyVBzG1lJnFAJcKAcxLS54N1ee1OQ=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "@typescript-eslint/typescript-estree": "8.43.0", "@typescript-eslint/utils": "8.43.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.38.0", "", {}, "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.41.0", "", {}, "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.41.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.41.0", "@typescript-eslint/tsconfig-utils": "8.41.0", "@typescript-eslint/types": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.43.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.43.0", "@typescript-eslint/tsconfig-utils": "8.43.0", "@typescript-eslint/types": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.38.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.41.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/types": "8.41.0", "@typescript-eslint/typescript-estree": "8.41.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.41.0", "", { "dependencies": { "@typescript-eslint/types": "8.41.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - "@vitejs/plugin-react": ["@vitejs/plugin-react@5.0.1", "", { "dependencies": { "@babel/core": "^7.28.3", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.32", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-DE4UNaBXwtVoDJ0ccBdLVjFTWL70NRuWNCxEieTI3lrq9ORB9aOCQEKstwDXBl87NvFdbqh/p7eINGyj0BthJA=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.0.2", "", { "dependencies": { "@babel/core": "^7.28.3", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.34", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-tmyFgixPZCx2+e6VO9TNITWcCQl8+Nl/E8YbAyPVv85QCc7/A3JrdfG2A8gIzvVhWuzMOVrFW1aReaNxrI6tbw=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], @@ -403,7 +403,7 @@ "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], - "axios": ["axios@1.11.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA=="], + "axios": ["axios@1.12.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw=="], "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], @@ -493,7 +493,7 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@9.34.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.34.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg=="], + "eslint": ["eslint@9.35.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.35.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg=="], "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], @@ -557,7 +557,7 @@ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - "globals": ["globals@16.3.0", "", {}, "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ=="], + "globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], @@ -581,7 +581,7 @@ "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], - "i18next": ["i18next@25.4.2", "", { "dependencies": { "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-gD4T25a6ovNXsfXY1TwHXXXLnD/K2t99jyYMCSimSCBnBRJVQr5j+VAaU83RJCPzrTGhVQ6dqIga66xO2rtd5g=="], + "i18next": ["i18next@25.5.2", "", { "dependencies": { "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-lW8Zeh37i/o0zVr+NoCHfNnfvVw+M6FQbRp36ZZ/NyHDJ3NJVpp2HhAUyU9WafL5AssymNoOjMRB48mmx2P6Hw=="], "i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.0", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g=="], @@ -665,9 +665,9 @@ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "lucide-react": ["lucide-react@0.541.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-s0Vircsu5WaGv2KoJZ5+SoxiAJ3UXV5KqEM3eIFDHaHkcLIFdIWgXtZ412+Gh02UsdS7Was+jvEpBvPCWQISlg=="], + "lucide-react": ["lucide-react@0.544.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw=="], - "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], + "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -793,7 +793,7 @@ "react-hook-form": ["react-hook-form@7.62.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA=="], - "react-i18next": ["react-i18next@15.7.2", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 25.4.1", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-xJxq7ibnhUlMvd82lNC4te1GxGUMoM1A05KKyqoqsBXVZtEvZg/fz/fnVzdlY/hhQ3SpP/79qCocZOtICGhd3g=="], + "react-i18next": ["react-i18next@15.7.3", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 25.4.1", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-AANws4tOE+QSq/IeMF/ncoHlMNZaVLxpa5uUGW1wjike68elVYr0018L9xYoqBr1OFO7G7boDPrbn0HpMCJxTw=="], "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], @@ -803,7 +803,7 @@ "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], - "react-router": ["react-router@7.8.2", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-7M2fR1JbIZ/jFWqelpvSZx+7vd7UlBTfdZqf6OSdF9g6+sfdqJDAWcak6ervbHph200ePlu+7G8LdoiC3ReyAQ=="], + "react-router": ["react-router@7.9.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-pfAByjcTpX55mqSDGwGnY9vDCpxqBLASg0BMNAuMmpSGESo/TaOUG6BllhAtAkCGx8Rnohik/XtaqiYUJtgW2g=="], "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], @@ -847,13 +847,13 @@ "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="], - "tailwindcss": ["tailwindcss@4.1.12", "", {}, "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA=="], + "tailwindcss": ["tailwindcss@4.1.13", "", {}, "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w=="], "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], "tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="], - "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -865,15 +865,15 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "tw-animate-css": ["tw-animate-css@1.3.7", "", {}, "sha512-lvLb3hTIpB5oGsk8JmLoAjeCHV58nKa2zHYn8yWOoG5JJusH3bhJlF2DLAZ/5NmJ+jyH3ssiAx/2KmbhavJy/A=="], + "tw-animate-css": ["tw-animate-css@1.3.8", "", {}, "sha512-Qrk3PZ7l7wUcGYhwZloqfkWCmaXZAoqjkdbIDvzfGshwGtexa/DAs9koXxIkrpEasyevandomzCBAV1Yyop5rw=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], - "typescript-eslint": ["typescript-eslint@8.41.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.41.0", "@typescript-eslint/parser": "8.41.0", "@typescript-eslint/typescript-estree": "8.41.0", "@typescript-eslint/utils": "8.41.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-n66rzs5OBXW3SFSnZHr2T685q1i4ODm2nulFJhMZBotaTavsS8TrI3d7bDlRSs9yWo7HmyWrN9qDu14Qv7Y0Dw=="], + "typescript-eslint": ["typescript-eslint@8.43.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.43.0", "@typescript-eslint/parser": "8.43.0", "@typescript-eslint/typescript-estree": "8.43.0", "@typescript-eslint/utils": "8.43.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-FyRGJKUGvcFekRRcBKFBlAhnp4Ng8rhe8tuvvkR9OiU0gfd4vyvTRQHEckO6VDlH57jbeUQem2IpqPq9kLJH+w=="], - "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + "undici-types": ["undici-types@7.11.0", "", {}, "sha512-kt1ZriHTi7MU+Z/r9DOdAI3ONdaR3M3csEaRc6ewa4f4dTvX4cQCbJ4NkEn0ohE4hHtq85+PhPSTY+pO/1PwgA=="], "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], @@ -899,7 +899,7 @@ "vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="], - "vite": ["vite@7.1.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw=="], + "vite": ["vite@7.1.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ=="], "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], @@ -911,7 +911,7 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "zod": ["zod@4.1.3", "", {}, "sha512-1neef4bMce1hNTrxvHVKxWjKfGDn0oAli3Wy1Uwb7TRO1+wEwoZUZNP1NXIEESybOBiFnBOhI6a4m6tCLE8dog=="], + "zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], @@ -935,10 +935,14 @@ "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], + "@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], + "@jridgewell/remapping/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="], "@jridgewell/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="], + "@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], + "@tailwindcss/node/jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" }, "bundled": true }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="], @@ -967,33 +971,35 @@ "@types/estree-jsx/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], - "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.41.0", "", { "dependencies": { "@typescript-eslint/types": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0" } }, "sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0" } }, "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg=="], - "@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.41.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/types": "8.41.0", "@typescript-eslint/typescript-estree": "8.41.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.43.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/types": "8.43.0", "@typescript-eslint/typescript-estree": "8.43.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.4", "", {}, "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="], - "@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.41.0", "", { "dependencies": { "@typescript-eslint/types": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0" } }, "sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ=="], + "@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0" } }, "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg=="], - "@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.41.0", "", {}, "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag=="], + "@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], - "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.41.0", "", {}, "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag=="], + "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], - "@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g=="], + "@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.41.0", "", { "dependencies": { "@typescript-eslint/types": "8.41.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg=="], - "@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.41.0", "", {}, "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag=="], + "@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], - "@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.41.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/types": "8.41.0", "@typescript-eslint/typescript-estree": "8.41.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A=="], + "@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.43.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/types": "8.43.0", "@typescript-eslint/typescript-estree": "8.43.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g=="], - "@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.41.0", "", {}, "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag=="], + "@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], - "@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.38.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.38.0", "@typescript-eslint/tsconfig-utils": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ=="], + "@typescript-eslint/utils/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="], + + "@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.41.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.41.0", "@typescript-eslint/tsconfig-utils": "8.41.0", "@typescript-eslint/types": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ=="], - "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.41.0", "", {}, "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag=="], + "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -1009,11 +1015,11 @@ "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - "tinyglobby/fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], + "typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.43.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/types": "8.43.0", "@typescript-eslint/typescript-estree": "8.43.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g=="], - "tinyglobby/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], + "@babel/generator/@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], - "typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.41.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/types": "8.41.0", "@typescript-eslint/typescript-estree": "8.41.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A=="], + "@babel/generator/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], "@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.27.1", "", { "dependencies": { "@babel/parser": "^7.27.1", "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w=="], @@ -1025,27 +1031,33 @@ "@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], - "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.41.0", "", {}, "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag=="], + "@jridgewell/remapping/@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], - "@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.41.0", "", {}, "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag=="], + "@jridgewell/remapping/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], - "@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.41.0", "", { "dependencies": { "@typescript-eslint/types": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0" } }, "sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], + + "@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], + + "@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0" } }, "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.38.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.38.0", "@typescript-eslint/types": "^8.38.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg=="], + "@typescript-eslint/utils/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.41.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.41.0", "@typescript-eslint/types": "^8.41.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ=="], - "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.38.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ=="], + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.41.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw=="], - "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g=="], + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.41.0", "", { "dependencies": { "@typescript-eslint/types": "8.41.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg=="], "@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], - "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.41.0", "", { "dependencies": { "@typescript-eslint/types": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0" } }, "sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ=="], + "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0" } }, "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg=="], - "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.41.0", "", {}, "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag=="], + "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], "@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], } diff --git a/frontend/package.json b/frontend/package.json index 3d3fc47d..6e63ee9b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,48 +10,48 @@ "preview": "vite preview" }, "dependencies": { - "@hookform/resolvers": "^5.2.1", + "@hookform/resolvers": "^5.2.2", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", - "@tailwindcss/vite": "^4.1.12", - "@tanstack/react-query": "^5.85.5", - "axios": "^1.11.0", + "@tailwindcss/vite": "^4.1.13", + "@tanstack/react-query": "^5.87.4", + "axios": "^1.12.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "i18next": "^25.4.2", + "i18next": "^25.5.2", "i18next-browser-languagedetector": "^8.2.0", "i18next-resources-to-backend": "^1.2.1", "input-otp": "^1.4.2", - "lucide-react": "^0.541.0", + "lucide-react": "^0.544.0", "next-themes": "^0.4.6", "react": "^19.1.1", "react-dom": "^19.1.1", "react-hook-form": "^7.62.0", - "react-i18next": "^15.7.2", + "react-i18next": "^15.7.3", "react-markdown": "^10.1.0", - "react-router": "^7.8.2", + "react-router": "^7.9.1", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", - "tailwindcss": "^4.1.12", - "zod": "^4.1.3" + "tailwindcss": "^4.1.13", + "zod": "^4.1.8" }, "devDependencies": { - "@eslint/js": "^9.34.0", - "@tanstack/eslint-plugin-query": "^5.83.1", - "@types/node": "^24.3.0", - "@types/react": "^19.1.11", - "@types/react-dom": "^19.1.8", - "@vitejs/plugin-react": "^5.0.1", - "eslint": "^9.34.0", + "@eslint/js": "^9.35.0", + "@tanstack/eslint-plugin-query": "^5.86.0", + "@types/node": "^24.4.0", + "@types/react": "^19.1.13", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.2", + "eslint": "^9.35.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", - "globals": "^16.3.0", + "globals": "^16.4.0", "prettier": "3.6.2", - "tw-animate-css": "^1.3.7", + "tw-animate-css": "^1.3.8", "typescript": "~5.9.2", - "typescript-eslint": "^8.41.0", - "vite": "^7.1.3" + "typescript-eslint": "^8.43.0", + "vite": "^7.1.5" } } \ No newline at end of file From b62b2932fec2a8378e7d2d826c79d53c6d613bd0 Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 16 Sep 2025 15:14:36 +0300 Subject: [PATCH 029/127] fix: only set trusted proxies if config option is not empty --- internal/bootstrap/app_bootstrap.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index 5301a768..18364da3 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -182,7 +182,10 @@ func (app *BootstrapApp) Setup() error { // Create engine engine := gin.New() - engine.SetTrustedProxies(strings.Split(app.Config.TrustedProxies, ",")) + + if len(app.Config.TrustedProxies) > 0 { + engine.SetTrustedProxies(strings.Split(app.Config.TrustedProxies, ",")) + } if config.Version != "development" { gin.SetMode(gin.ReleaseMode) From b2dcffdbe4a0b27391068a37e36935f8bc0e2e28 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 18:54:53 +0300 Subject: [PATCH 030/127] chore(deps): bump gorm.io/gorm in the minor-patch group (#360) Bumps the minor-patch group with 1 update: [gorm.io/gorm](https://github.com/go-gorm/gorm). Updates `gorm.io/gorm` from 1.30.5 to 1.31.0 - [Release notes](https://github.com/go-gorm/gorm/releases) - [Commits](https://github.com/go-gorm/gorm/compare/v1.30.5...v1.31.0) --- updated-dependencies: - dependency-name: gorm.io/gorm dependency-version: 1.31.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7d2e7b9a..7ec10480 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/weppos/publicsuffix-go v0.50.0 golang.org/x/crypto v0.42.0 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b - gorm.io/gorm v1.30.5 + gorm.io/gorm v1.31.0 gotest.tools/v3 v3.5.2 ) diff --git a/go.sum b/go.sum index 8147ebe4..1f40013f 100644 --- a/go.sum +++ b/go.sum @@ -346,8 +346,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/gorm v1.30.5 h1:dvEfYwxL+i+xgCNSGGBT1lDjCzfELK8fHZxL3Ee9X0s= -gorm.io/gorm v1.30.5/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= +gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= From 51937906ad0a4449ba43fb497c4c912c3851cb15 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 18:55:21 +0300 Subject: [PATCH 031/127] chore(deps): bump the minor-patch group across 1 directory with 7 updates (#362) Bumps the minor-patch group with 7 updates in the /frontend directory: | Package | From | To | | --- | --- | --- | | [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.87.4` | `5.89.0` | | [zod](https://github.com/colinhacks/zod) | `4.1.8` | `4.1.9` | | [@tanstack/eslint-plugin-query](https://github.com/TanStack/query/tree/HEAD/packages/eslint-plugin-query) | `5.86.0` | `5.89.0` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `24.4.0` | `24.5.2` | | [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `5.0.2` | `5.0.3` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.43.0` | `8.44.0` | | [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.1.5` | `7.1.6` | Updates `@tanstack/react-query` from 5.87.4 to 5.89.0 - [Release notes](https://github.com/TanStack/query/releases) - [Commits](https://github.com/TanStack/query/commits/v5.89.0/packages/react-query) Updates `zod` from 4.1.8 to 4.1.9 - [Release notes](https://github.com/colinhacks/zod/releases) - [Commits](https://github.com/colinhacks/zod/compare/v4.1.8...v4.1.9) Updates `@tanstack/eslint-plugin-query` from 5.86.0 to 5.89.0 - [Release notes](https://github.com/TanStack/query/releases) - [Commits](https://github.com/TanStack/query/commits/v5.89.0/packages/eslint-plugin-query) Updates `@types/node` from 24.4.0 to 24.5.2 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@vitejs/plugin-react` from 5.0.2 to 5.0.3 - [Release notes](https://github.com/vitejs/vite-plugin-react/releases) - [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@5.0.3/packages/plugin-react) Updates `typescript-eslint` from 8.43.0 to 8.44.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.44.0/packages/typescript-eslint) Updates `vite` from 7.1.5 to 7.1.6 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v7.1.6/packages/vite) --- updated-dependencies: - dependency-name: "@tanstack/react-query" dependency-version: 5.89.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: zod dependency-version: 4.1.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: "@tanstack/eslint-plugin-query" dependency-version: 5.89.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@types/node" dependency-version: 24.5.2 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@vitejs/plugin-react" dependency-version: 5.0.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: typescript-eslint dependency-version: 8.44.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: vite dependency-version: 7.1.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/bun.lock | 144 ++++++++++++++++++++---------------------- frontend/package.json | 14 ++-- 2 files changed, 77 insertions(+), 81 deletions(-) diff --git a/frontend/bun.lock b/frontend/bun.lock index 037318cd..a190e7b4 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -10,7 +10,7 @@ "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@tailwindcss/vite": "^4.1.13", - "@tanstack/react-query": "^5.87.4", + "@tanstack/react-query": "^5.89.0", "axios": "^1.12.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -29,15 +29,15 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.13", - "zod": "^4.1.8", + "zod": "^4.1.9", }, "devDependencies": { "@eslint/js": "^9.35.0", - "@tanstack/eslint-plugin-query": "^5.86.0", - "@types/node": "^24.4.0", + "@tanstack/eslint-plugin-query": "^5.89.0", + "@types/node": "^24.5.2", "@types/react": "^19.1.13", "@types/react-dom": "^19.1.9", - "@vitejs/plugin-react": "^5.0.2", + "@vitejs/plugin-react": "^5.0.3", "eslint": "^9.35.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", @@ -45,19 +45,17 @@ "prettier": "3.6.2", "tw-animate-css": "^1.3.8", "typescript": "~5.9.2", - "typescript-eslint": "^8.43.0", - "vite": "^7.1.5", + "typescript-eslint": "^8.44.0", + "vite": "^7.1.6", }, }, }, "packages": { - "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], - "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], "@babel/compat-data": ["@babel/compat-data@7.27.2", "", {}, "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ=="], - "@babel/core": ["@babel/core@7.28.3", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.3", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ=="], + "@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="], "@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="], @@ -77,9 +75,9 @@ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - "@babel/helpers": ["@babel/helpers@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.2" } }, "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw=="], + "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], - "@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="], + "@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="], "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], @@ -89,9 +87,9 @@ "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], - "@babel/traverse": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="], + "@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="], - "@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], + "@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], @@ -181,7 +179,7 @@ "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -191,7 +189,7 @@ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -255,7 +253,7 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.34", "", {}, "sha512-LyAREkZHP5pMom7c24meKmJCdhf2hEyvam2q0unr3or9ydwDL+DJ8chTF6Av/RFPb3rH8UFBdMzO5MxTZW97oA=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.35", "", {}, "sha512-slYrCpoxJUqzFDDNlvrOYRazQUNRvWPjXA17dAOISY3rDMxX6k8K4cj2H+hEYMHF81HO3uNd5rHVigAWRM5dSg=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.46.2", "", { "os": "android", "cpu": "arm" }, "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA=="], @@ -329,11 +327,11 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.1.13", "", { "dependencies": { "@tailwindcss/node": "4.1.13", "@tailwindcss/oxide": "4.1.13", "tailwindcss": "4.1.13" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ=="], - "@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.86.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.37.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-tmXdnx/fF3yY5G5jpzrJQbASY3PNzsKF0gq9IsZVqz3LJ4sExgdUFGQ305nao0wTMBOclyrSC13v/VQ3yOXu/Q=="], + "@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.89.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.37.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-vz8TEuw9GO0xXIdreMpcofvOY17T3cjgob9bSFln8yQsKsbsUvtpvV3F8pVC3tZEDq0IwO++3/e0/+7YKEarNA=="], - "@tanstack/query-core": ["@tanstack/query-core@5.87.4", "", {}, "sha512-uNsg6zMxraEPDVO2Bn+F3/ctHi+Zsk+MMpcN8h6P7ozqD088F6mFY5TfGM7zuyIrL7HKpDyu6QHfLWiDxh3cuw=="], + "@tanstack/query-core": ["@tanstack/query-core@5.89.0", "", {}, "sha512-joFV1MuPhSLsKfTzwjmPDrp8ENfZ9N23ymFu07nLfn3JCkSHy0CFgsyhHTJOmWaumC/WiNIKM0EJyduCF/Ih/Q=="], - "@tanstack/react-query": ["@tanstack/react-query@5.87.4", "", { "dependencies": { "@tanstack/query-core": "5.87.4" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-T5GT/1ZaNsUXf5I3RhcYuT17I4CPlbZgyLxc/ZGv7ciS6esytlbjb3DgUFO6c8JWYMDpdjSWInyGZUErgzqhcA=="], + "@tanstack/react-query": ["@tanstack/react-query@5.89.0", "", { "dependencies": { "@tanstack/query-core": "5.89.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-SXbtWSTSRXyBOe80mszPxpEbaN4XPRUp/i0EfQK1uyj3KCk/c8FuPJNIRwzOVe/OU3rzxrYtiNabsAmk1l714A=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -357,7 +355,7 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@24.4.0", "", { "dependencies": { "undici-types": "~7.11.0" } }, "sha512-gUuVEAK4/u6F9wRLznPUU4WGUacSEBDPoC2TrBkw3GAnOLHBL45QdfHOXp1kJ4ypBGLxTOB+t7NJLpKoC3gznQ=="], + "@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="], "@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="], @@ -365,29 +363,29 @@ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.43.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/type-utils": "8.43.0", "@typescript-eslint/utils": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.43.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.44.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/type-utils": "8.44.0", "@typescript-eslint/utils": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.44.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-EGDAOGX+uwwekcS0iyxVDmRV9HX6FLSM5kzrAToLTsr9OWCIKG/y3lQheCq18yZ5Xh78rRKJiEpP0ZaCs4ryOQ=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.43.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/types": "8.43.0", "@typescript-eslint/typescript-estree": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.44.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/types": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.43.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.43.0", "@typescript-eslint/types": "^8.43.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.44.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.44.0", "@typescript-eslint/types": "^8.44.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ZeaGNraRsq10GuEohKTo4295Z/SuGcSq2LzfGlqiuEvfArzo/VRrT0ZaJsVPuKZ55lVbNk8U6FcL+ZMH8CoyVA=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.41.0", "", { "dependencies": { "@typescript-eslint/types": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0" } }, "sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0" } }, "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.43.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.44.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-x5Y0+AuEPqAInc6yd0n5DAcvtoQ/vyaGwuX5HE9n6qAefk1GaedqrLQF8kQGylLUb9pnZyLf+iEiL9fr8APDtQ=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "@typescript-eslint/typescript-estree": "8.43.0", "@typescript-eslint/utils": "8.43.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.44.0", "", { "dependencies": { "@typescript-eslint/types": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0", "@typescript-eslint/utils": "8.44.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-9cwsoSxJ8Sak67Be/hD2RNt/fsqmWnNE1iHohG8lxqLSNY8xNfyY7wloo5zpW3Nu9hxVgURevqfcH6vvKCt6yg=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.41.0", "", {}, "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.43.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.43.0", "@typescript-eslint/tsconfig-utils": "8.43.0", "@typescript-eslint/types": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.44.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.44.0", "@typescript-eslint/tsconfig-utils": "8.44.0", "@typescript-eslint/types": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-lqNj6SgnGcQZwL4/SBJ3xdPEfcBuhCG8zdcwCPgYcmiPLgokiNDKlbPzCwEwu7m279J/lBYWtDYL+87OEfn8Jw=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.41.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/types": "8.41.0", "@typescript-eslint/typescript-estree": "8.41.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.43.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/types": "8.43.0", "@typescript-eslint/typescript-estree": "8.43.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.44.0", "", { "dependencies": { "@typescript-eslint/types": "8.44.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-zaz9u8EJ4GBmnehlrpoKvj/E3dNbuQ7q0ucyZImm3cLqJ8INTc970B1qEqDX/Rzq65r3TvVTN7kHWPBoyW7DWw=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - "@vitejs/plugin-react": ["@vitejs/plugin-react@5.0.2", "", { "dependencies": { "@babel/core": "^7.28.3", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.34", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-tmyFgixPZCx2+e6VO9TNITWcCQl8+Nl/E8YbAyPVv85QCc7/A3JrdfG2A8gIzvVhWuzMOVrFW1aReaNxrI6tbw=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.0.3", "", { "dependencies": { "@babel/core": "^7.28.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.35", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-PFVHhosKkofGH0Yzrw1BipSedTH68BFF8ZWy1kfUpCtJcouXXY0+racG8sExw7hw0HoX36813ga5o3LTWZ4FUg=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], @@ -871,9 +869,9 @@ "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], - "typescript-eslint": ["typescript-eslint@8.43.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.43.0", "@typescript-eslint/parser": "8.43.0", "@typescript-eslint/typescript-estree": "8.43.0", "@typescript-eslint/utils": "8.43.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-FyRGJKUGvcFekRRcBKFBlAhnp4Ng8rhe8tuvvkR9OiU0gfd4vyvTRQHEckO6VDlH57jbeUQem2IpqPq9kLJH+w=="], + "typescript-eslint": ["typescript-eslint@8.44.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.44.0", "@typescript-eslint/parser": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0", "@typescript-eslint/utils": "8.44.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ib7mCkYuIzYonCq9XWF5XNw+fkj2zg629PSa9KNIQ47RXFF763S5BIX4wqz1+FLPogTZoiw8KmCiRPRa8bL3qw=="], - "undici-types": ["undici-types@7.11.0", "", {}, "sha512-kt1ZriHTi7MU+Z/r9DOdAI3ONdaR3M3csEaRc6ewa4f4dTvX4cQCbJ4NkEn0ohE4hHtq85+PhPSTY+pO/1PwgA=="], + "undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="], "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], @@ -899,7 +897,7 @@ "vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="], - "vite": ["vite@7.1.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ=="], + "vite": ["vite@7.1.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-SRYIB8t/isTwNn8vMB3MR6E+EQZM/WG1aKmmIUCfDXfVvKfc20ZpamngWHKzAmmu9ppsgxsg4b2I7c90JZudIQ=="], "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], @@ -911,18 +909,20 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], + "zod": ["zod@4.1.9", "", {}, "sha512-HI32jTq0AUAC125z30E8bQNz0RQ+9Uc+4J7V97gLYjZVKRjeydPgGt6dvQzFrav7MYOUGFqqOGiHpA/fdbd0cQ=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - "@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="], + "@babel/generator/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="], - "@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="], + "@babel/generator/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], "@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.27.1", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.1", "@babel/parser": "^7.27.1", "@babel/template": "^7.27.1", "@babel/types": "^7.27.1", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg=="], "@babel/helper-module-imports/@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="], + "@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="], + "@babel/template/@babel/parser": ["@babel/parser@7.27.2", "", { "dependencies": { "@babel/types": "^7.27.1" }, "bin": "./bin/babel-parser.js" }, "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw=="], "@babel/template/@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="], @@ -937,10 +937,6 @@ "@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], - "@jridgewell/remapping/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="], - - "@jridgewell/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="], - "@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], "@tailwindcss/node/jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="], @@ -971,35 +967,33 @@ "@types/estree-jsx/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], - "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0" } }, "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.44.0", "", { "dependencies": { "@typescript-eslint/types": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0" } }, "sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA=="], - "@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.43.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/types": "8.43.0", "@typescript-eslint/typescript-estree": "8.43.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.44.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/types": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.4", "", {}, "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="], - "@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0" } }, "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg=="], + "@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.44.0", "", { "dependencies": { "@typescript-eslint/types": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0" } }, "sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA=="], - "@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], + "@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="], - "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], + "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="], - "@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.41.0", "", { "dependencies": { "@typescript-eslint/types": "8.41.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg=="], + "@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw=="], - "@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], + "@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="], - "@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.43.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/types": "8.43.0", "@typescript-eslint/typescript-estree": "8.43.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g=="], + "@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.44.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/types": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg=="], - "@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], + "@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], - "@typescript-eslint/utils/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="], + "@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.43.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.43.0", "@typescript-eslint/tsconfig-utils": "8.43.0", "@typescript-eslint/types": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw=="], - "@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.41.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.41.0", "@typescript-eslint/tsconfig-utils": "8.41.0", "@typescript-eslint/types": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ=="], - - "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], + "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -1015,11 +1009,7 @@ "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - "typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.43.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/types": "8.43.0", "@typescript-eslint/typescript-estree": "8.43.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g=="], - - "@babel/generator/@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], - - "@babel/generator/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], + "typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.44.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/types": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg=="], "@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.27.1", "", { "dependencies": { "@babel/parser": "^7.27.1", "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w=="], @@ -1027,38 +1017,44 @@ "@babel/helper-module-imports/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], - "@eslint/eslintrc/espree/acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], + "@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="], - "@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], + "@babel/helper-module-transforms/@babel/traverse/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], - "@jridgewell/remapping/@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], + "@eslint/eslintrc/espree/acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], - "@jridgewell/remapping/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], + "@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], - "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="], - "@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="], - "@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0" } }, "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg=="], + "@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.44.0", "", { "dependencies": { "@typescript-eslint/types": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0" } }, "sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - "@typescript-eslint/utils/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - - "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.41.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.41.0", "@typescript-eslint/types": "^8.41.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ=="], + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.43.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.43.0", "@typescript-eslint/types": "^8.43.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw=="], - "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.41.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw=="], + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.43.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA=="], - "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.41.0", "", { "dependencies": { "@typescript-eslint/types": "8.41.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg=="], + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw=="], "@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], - "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0" } }, "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg=="], + "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.44.0", "", { "dependencies": { "@typescript-eslint/types": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0" } }, "sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA=="], - "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], + "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="], + + "@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], + + "@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], "@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], + + "@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], } } diff --git a/frontend/package.json b/frontend/package.json index 6e63ee9b..211dea4b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,7 @@ "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@tailwindcss/vite": "^4.1.13", - "@tanstack/react-query": "^5.87.4", + "@tanstack/react-query": "^5.89.0", "axios": "^1.12.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -35,15 +35,15 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.13", - "zod": "^4.1.8" + "zod": "^4.1.9" }, "devDependencies": { "@eslint/js": "^9.35.0", - "@tanstack/eslint-plugin-query": "^5.86.0", - "@types/node": "^24.4.0", + "@tanstack/eslint-plugin-query": "^5.89.0", + "@types/node": "^24.5.2", "@types/react": "^19.1.13", "@types/react-dom": "^19.1.9", - "@vitejs/plugin-react": "^5.0.2", + "@vitejs/plugin-react": "^5.0.3", "eslint": "^9.35.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", @@ -51,7 +51,7 @@ "prettier": "3.6.2", "tw-animate-css": "^1.3.8", "typescript": "~5.9.2", - "typescript-eslint": "^8.43.0", - "vite": "^7.1.5" + "typescript-eslint": "^8.44.0", + "vite": "^7.1.6" } } \ No newline at end of file From 50105e4e9d7844495f8a7ec4643ef1c116f0f39b Mon Sep 17 00:00:00 2001 From: Stavros Date: Fri, 19 Sep 2025 14:44:22 +0300 Subject: [PATCH 032/127] feat: version info analytics (#363) * feat: version info analytics * refactor: don't create new client everytime --- cmd/root.go | 1 + internal/bootstrap/app_bootstrap.go | 129 +++++++++++++++++++------- internal/config/config.go | 5 + internal/utils/security_utils.go | 5 +- internal/utils/security_utils_test.go | 11 +-- 5 files changed, 109 insertions(+), 42 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index aeb96a59..c81a52a3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -94,6 +94,7 @@ func init() { {"resources-dir", "/data/resources", "Path to a directory containing custom resources (e.g. background image)."}, {"database-path", "/data/tinyauth.db", "Path to the Sqlite database file."}, {"trusted-proxies", "", "Comma separated list of trusted proxies (IP addresses or CIDRs) for correct client IP detection."}, + {"disable-analytics", false, "Disable anonymous version collection."}, } for _, opt := range configOptions { diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index 18364da3..d1f33738 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -1,10 +1,14 @@ package bootstrap import ( + "bytes" + "encoding/json" "fmt" + "net/http" "net/url" "os" "strings" + "time" "tinyauth/internal/config" "tinyauth/internal/controller" "tinyauth/internal/middleware" @@ -29,40 +33,43 @@ type Service interface { } type BootstrapApp struct { - Config config.Config + config config.Config + uuid string } func NewBootstrapApp(config config.Config) *BootstrapApp { return &BootstrapApp{ - Config: config, + config: config, } } func (app *BootstrapApp) Setup() error { // Parse users - users, err := utils.GetUsers(app.Config.Users, app.Config.UsersFile) + users, err := utils.GetUsers(app.config.Users, app.config.UsersFile) if err != nil { return err } // Get OAuth configs - oauthProviders, err := utils.GetOAuthProvidersConfig(os.Environ(), os.Args, app.Config.AppURL) + oauthProviders, err := utils.GetOAuthProvidersConfig(os.Environ(), os.Args, app.config.AppURL) if err != nil { return err } // Get cookie domain - cookieDomain, err := utils.GetCookieDomain(app.Config.AppURL) + cookieDomain, err := utils.GetCookieDomain(app.config.AppURL) if err != nil { return err } // Cookie names - appUrl, _ := url.Parse(app.Config.AppURL) // Already validated - cookieId := utils.GenerateIdentifier(appUrl.Hostname()) + appUrl, _ := url.Parse(app.config.AppURL) // Already validated + uuid := utils.GenerateUUID(appUrl.Hostname()) + app.uuid = uuid + cookieId := strings.Split(uuid, "-")[0] sessionCookieName := fmt.Sprintf("%s-%s", config.SessionCookieName, cookieId) csrfCookieName := fmt.Sprintf("%s-%s", config.CSRFCookieName, cookieId) redirectCookieName := fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId) @@ -70,26 +77,26 @@ func (app *BootstrapApp) Setup() error { // Create configs authConfig := service.AuthServiceConfig{ Users: users, - OauthWhitelist: app.Config.OAuthWhitelist, - SessionExpiry: app.Config.SessionExpiry, - SecureCookie: app.Config.SecureCookie, + OauthWhitelist: app.config.OAuthWhitelist, + SessionExpiry: app.config.SessionExpiry, + SecureCookie: app.config.SecureCookie, CookieDomain: cookieDomain, - LoginTimeout: app.Config.LoginTimeout, - LoginMaxRetries: app.Config.LoginMaxRetries, + LoginTimeout: app.config.LoginTimeout, + LoginMaxRetries: app.config.LoginMaxRetries, SessionCookieName: sessionCookieName, } // Setup services var ldapService *service.LdapService - if app.Config.LdapAddress != "" { + if app.config.LdapAddress != "" { ldapConfig := service.LdapServiceConfig{ - Address: app.Config.LdapAddress, - BindDN: app.Config.LdapBindDN, - BindPassword: app.Config.LdapBindPassword, - BaseDN: app.Config.LdapBaseDN, - Insecure: app.Config.LdapInsecure, - SearchFilter: app.Config.LdapSearchFilter, + Address: app.config.LdapAddress, + BindDN: app.config.LdapBindDN, + BindPassword: app.config.LdapBindPassword, + BaseDN: app.config.LdapBaseDN, + Insecure: app.config.LdapInsecure, + SearchFilter: app.config.LdapSearchFilter, } ldapService = service.NewLdapService(ldapConfig) @@ -104,7 +111,7 @@ func (app *BootstrapApp) Setup() error { // Bootstrap database databaseService := service.NewDatabaseService(service.DatabaseServiceConfig{ - DatabasePath: app.Config.DatabasePath, + DatabasePath: app.config.DatabasePath, }) log.Debug().Str("service", fmt.Sprintf("%T", databaseService)).Msg("Initializing service") @@ -183,8 +190,8 @@ func (app *BootstrapApp) Setup() error { // Create engine engine := gin.New() - if len(app.Config.TrustedProxies) > 0 { - engine.SetTrustedProxies(strings.Split(app.Config.TrustedProxies, ",")) + if len(app.config.TrustedProxies) > 0 { + engine.SetTrustedProxies(strings.Split(app.config.TrustedProxies, ",")) } if config.Version != "development" { @@ -219,24 +226,24 @@ func (app *BootstrapApp) Setup() error { // Create controllers contextController := controller.NewContextController(controller.ContextControllerConfig{ Providers: configuredProviders, - Title: app.Config.Title, - AppURL: app.Config.AppURL, + Title: app.config.Title, + AppURL: app.config.AppURL, CookieDomain: cookieDomain, - ForgotPasswordMessage: app.Config.ForgotPasswordMessage, - BackgroundImage: app.Config.BackgroundImage, - OAuthAutoRedirect: app.Config.OAuthAutoRedirect, + ForgotPasswordMessage: app.config.ForgotPasswordMessage, + BackgroundImage: app.config.BackgroundImage, + OAuthAutoRedirect: app.config.OAuthAutoRedirect, }, apiRouter) oauthController := controller.NewOAuthController(controller.OAuthControllerConfig{ - AppURL: app.Config.AppURL, - SecureCookie: app.Config.SecureCookie, + AppURL: app.config.AppURL, + SecureCookie: app.config.SecureCookie, CSRFCookieName: csrfCookieName, RedirectCookieName: redirectCookieName, CookieDomain: cookieDomain, }, apiRouter, authService, oauthBrokerService) proxyController := controller.NewProxyController(controller.ProxyControllerConfig{ - AppURL: app.Config.AppURL, + AppURL: app.config.AppURL, }, apiRouter, dockerService, authService) userController := controller.NewUserController(controller.UserControllerConfig{ @@ -244,7 +251,7 @@ func (app *BootstrapApp) Setup() error { }, apiRouter, authService) resourcesController := controller.NewResourcesController(controller.ResourcesControllerConfig{ - ResourcesDir: app.Config.ResourcesDir, + ResourcesDir: app.config.ResourcesDir, }, mainRouter) healthController := controller.NewHealthController(apiRouter) @@ -264,8 +271,14 @@ func (app *BootstrapApp) Setup() error { ctrl.SetupRoutes() } + // If analytics are not disabled, start heartbeat + if !app.config.DisableAnalytics { + log.Debug().Msg("Starting heartbeat routine") + go app.heartbeat() + } + // Start server - address := fmt.Sprintf("%s:%d", app.Config.Address, app.Config.Port) + address := fmt.Sprintf("%s:%d", app.config.Address, app.config.Port) log.Info().Msgf("Starting server on %s", address) if err := engine.Run(address); err != nil { log.Fatal().Err(err).Msg("Failed to start server") @@ -273,3 +286,55 @@ func (app *BootstrapApp) Setup() error { return nil } + +func (app *BootstrapApp) heartbeat() { + ticker := time.NewTicker(time.Duration(12) * time.Hour) + defer ticker.Stop() + + type heartbeat struct { + UUID string `json:"uuid"` + Version string `json:"version"` + } + + var body heartbeat + + body.UUID = app.uuid + body.Version = config.Version + + bodyJson, err := json.Marshal(body) + + if err != nil { + log.Error().Err(err).Msg("Failed to marshal heartbeat body") + return + } + + client := &http.Client{} + + heartbeatURL := config.ApiServer + "/v1/instances/heartbeat" + + for ; true; <-ticker.C { + log.Debug().Msg("Sending heartbeat") + + req, err := http.NewRequest(http.MethodPost, heartbeatURL, bytes.NewReader(bodyJson)) + + if err != nil { + log.Error().Err(err).Msg("Failed to create heartbeat request") + continue + } + + req.Header.Add("Content-Type", "application/json") + + res, err := client.Do(req) + + if err != nil { + log.Error().Err(err).Msg("Failed to send heartbeat") + continue + } + + res.Body.Close() + + if res.StatusCode != 200 { + log.Debug().Str("status", res.Status).Msg("Heartbeat returned non-200 status") + } + } +} diff --git a/internal/config/config.go b/internal/config/config.go index 4fc66fcd..e969ad0f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -39,6 +39,7 @@ type Config struct { ResourcesDir string `mapstructure:"resources-dir"` DatabasePath string `mapstructure:"database-path" validate:"required"` TrustedProxies string `mapstructure:"trusted-proxies"` + DisableAnalytics bool `mapstructure:"disable-analytics"` } // OAuth/OIDC config @@ -169,3 +170,7 @@ type AppPath struct { type Providers struct { Providers map[string]OAuthServiceConfig } + +// API server + +var ApiServer = "https://api.tinyauth.app" diff --git a/internal/utils/security_utils.go b/internal/utils/security_utils.go index 91e17ee1..40fe7130 100644 --- a/internal/utils/security_utils.go +++ b/internal/utils/security_utils.go @@ -101,8 +101,7 @@ func CheckFilter(filter string, str string) bool { return false } -func GenerateIdentifier(str string) string { +func GenerateUUID(str string) string { uuid := uuid.NewSHA1(uuid.NameSpaceURL, []byte(str)) - uuidString := uuid.String() - return strings.Split(uuidString, "-")[0] + return uuid.String() } diff --git a/internal/utils/security_utils_test.go b/internal/utils/security_utils_test.go index 941f853f..9adcd7ca 100644 --- a/internal/utils/security_utils_test.go +++ b/internal/utils/security_utils_test.go @@ -136,16 +136,13 @@ func TestCheckFilter(t *testing.T) { assert.Equal(t, false, utils.CheckFilter("apple, banana, cherry", "grape")) } -func TestGenerateIdentifier(t *testing.T) { +func TestGenerateUUID(t *testing.T) { // Consistent output for same input - id1 := utils.GenerateIdentifier("teststring") - id2 := utils.GenerateIdentifier("teststring") + id1 := utils.GenerateUUID("teststring") + id2 := utils.GenerateUUID("teststring") assert.Equal(t, id1, id2) // Different output for different input - id3 := utils.GenerateIdentifier("differentstring") + id3 := utils.GenerateUUID("differentstring") assert.Assert(t, id1 != id3) - - // Check length (should be 8 characters from first segment of UUID) - assert.Equal(t, 8, len(id1)) } From d70cbea546afeef587be636b7d23bb0aa139ba68 Mon Sep 17 00:00:00 2001 From: Stavros Date: Fri, 19 Sep 2025 14:53:10 +0300 Subject: [PATCH 033/127] chore: handle trusted proxies config error --- internal/bootstrap/app_bootstrap.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index d1f33738..3416ff21 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -191,7 +191,11 @@ func (app *BootstrapApp) Setup() error { engine := gin.New() if len(app.config.TrustedProxies) > 0 { - engine.SetTrustedProxies(strings.Split(app.config.TrustedProxies, ",")) + err := engine.SetTrustedProxies(strings.Split(app.config.TrustedProxies, ",")) + + if err != nil { + return fmt.Errorf("failed to set trusted proxies: %w", err) + } } if config.Version != "development" { From 866933b3d687a4892b554ddc4efd9a072479732e Mon Sep 17 00:00:00 2001 From: Stavros Date: Fri, 19 Sep 2025 15:38:32 +0300 Subject: [PATCH 034/127] fix: fix version handling in docker and cd --- .github/workflows/nightly.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- Dockerfile | 2 +- docker-compose.dev.yml | 4 ++++ internal/config/config.go | 4 ++-- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 9c46d895..75ae89f1 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -80,7 +80,7 @@ jobs: - name: Build run: | cp -r frontend/dist internal/assets/dist - go build -ldflags "-s -w -X tinyauth/internal/constants.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/constants.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/constants.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 + go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 env: CGO_ENABLED: 0 @@ -126,7 +126,7 @@ jobs: - name: Build run: | cp -r frontend/dist internal/assets/dist - go build -ldflags "-s -w -X tinyauth/internal/constants.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/constants.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/constants.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 + go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 env: CGO_ENABLED: 0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6f6ae9b0..1a1bd25e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,7 +58,7 @@ jobs: - name: Build run: | cp -r frontend/dist internal/assets/dist - go build -ldflags "-s -w -X tinyauth/internal/constants.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/constants.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/constants.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 + go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 env: CGO_ENABLED: 0 @@ -101,7 +101,7 @@ jobs: - name: Build run: | cp -r frontend/dist internal/assets/dist - go build -ldflags "-s -w -X tinyauth/internal/constants.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/constants.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/constants.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 + go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 env: CGO_ENABLED: 0 diff --git a/Dockerfile b/Dockerfile index 55d8fa11..2bbe0581 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,7 +38,7 @@ COPY ./cmd ./cmd COPY ./internal ./internal COPY --from=frontend-builder /frontend/dist ./internal/assets/dist -RUN CGO_ENABLED=0 go build -ldflags "-s -w -X tinyauth/internal/constants.Version=${VERSION} -X tinyauth/internal/constants.CommitHash=${COMMIT_HASH} -X tinyauth/internal/constants.BuildTimestamp=${BUILD_TIMESTAMP}" +RUN CGO_ENABLED=0 go build -ldflags "-s -w -X tinyauth/internal/config.Version=${VERSION} -X tinyauth/internal/config.CommitHash=${COMMIT_HASH} -X tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" # Runner FROM alpine:3.22 AS runner diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 3cf837c7..cc454f6a 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -34,6 +34,10 @@ services: build: context: . dockerfile: Dockerfile.dev + args: + - VERSION=development + - COMMIT_HASH=development + - BUILD_TIMESTAMP=000-00-00T00:00:00Z env_file: .env volumes: - ./internal:/tinyauth/internal diff --git a/internal/config/config.go b/internal/config/config.go index e969ad0f..32e586e7 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,8 +3,8 @@ package config // Version information, set at build time var Version = "development" -var CommitHash = "n/a" -var BuildTimestamp = "n/a" +var CommitHash = "development" +var BuildTimestamp = "0000-00-00T00:00:00Z" // Cookie name templates From c9867ccb76c04977d10fa574278df287f4c4c0b7 Mon Sep 17 00:00:00 2001 From: Stavros Date: Sat, 20 Sep 2025 11:08:57 +0300 Subject: [PATCH 035/127] chore: fix typo --- internal/utils/decoders/decoders.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/utils/decoders/decoders.go b/internal/utils/decoders/decoders.go index 72a11d5b..74937175 100644 --- a/internal/utils/decoders/decoders.go +++ b/internal/utils/decoders/decoders.go @@ -18,10 +18,10 @@ func NormalizeKeys(keys map[string]string, rootName string, sep string) map[stri finalKey = append(finalKey, rootName) finalKey = append(finalKey, "providers") - cebabKey := strings.ToLower(k) + kebabKey := strings.ToLower(k) for _, known := range knownKeys { - if strings.HasSuffix(cebabKey, strings.ReplaceAll(known, "-", sep)) { + if strings.HasSuffix(kebabKey, strings.ReplaceAll(known, "-", sep)) { suffix = known break } @@ -31,7 +31,7 @@ func NormalizeKeys(keys map[string]string, rootName string, sep string) map[stri continue } - clientNameParts := strings.Split(strings.TrimPrefix(strings.TrimSuffix(cebabKey, sep+strings.ReplaceAll(suffix, "-", sep)), "providers"+sep), sep) + clientNameParts := strings.Split(strings.TrimPrefix(strings.TrimSuffix(kebabKey, sep+strings.ReplaceAll(suffix, "-", sep)), "providers"+sep), sep) for i, p := range clientNameParts { if i == 0 { @@ -46,9 +46,9 @@ func NormalizeKeys(keys map[string]string, rootName string, sep string) map[stri finalKey = append(finalKey, camelClientName) - filedParts := strings.Split(suffix, "-") + fieldParts := strings.Split(suffix, "-") - for i, p := range filedParts { + for i, p := range fieldParts { if i == 0 { camelField += p continue From e114bf09434b18a763acc6f7a2258fb6954a58d4 Mon Sep 17 00:00:00 2001 From: axjp <63340001+Alpha-404@users.noreply.github.com> Date: Sun, 21 Sep 2025 07:52:41 +0100 Subject: [PATCH 036/127] Update verify.go (#364) --- cmd/user/verify/verify.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/user/verify/verify.go b/cmd/user/verify/verify.go index b10ff705..4bff6ed1 100644 --- a/cmd/user/verify/verify.go +++ b/cmd/user/verify/verify.go @@ -70,7 +70,7 @@ var VerifyCmd = &cobra.Command{ err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(iPassword)) if err != nil { - log.Fatal().Msg("Ppassword is incorrect") + log.Fatal().Msg("Password is incorrect") } if user.TotpSecret == "" { From f8047a6c2ee92a6612569a62044de5c8e9014f40 Mon Sep 17 00:00:00 2001 From: Stavros Date: Mon, 22 Sep 2025 15:52:43 +0300 Subject: [PATCH 037/127] feat: add option to disable resources server --- .env.example | 19 ++++--------------- cmd/root.go | 1 + internal/bootstrap/app_bootstrap.go | 3 ++- internal/config/config.go | 1 + internal/controller/resources_controller.go | 10 +++++++++- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.env.example b/.env.example index 63cececa..4d40c811 100644 --- a/.env.example +++ b/.env.example @@ -4,20 +4,6 @@ APP_URL=http://localhost:3000 USERS=your_user_password_hash USERS_FILE=users_file SECURE_COOKIE=false -GITHUB_CLIENT_ID=github_client_id -GITHUB_CLIENT_SECRET=github_client_secret -GITHUB_CLIENT_SECRET_FILE=github_client_secret_file -GOOGLE_CLIENT_ID=google_client_id -GOOGLE_CLIENT_SECRET=google_client_secret -GOOGLE_CLIENT_SECRET_FILE=google_client_secret_file -GENERIC_CLIENT_ID=generic_client_id -GENERIC_CLIENT_SECRET=generic_client_secret -GENERIC_CLIENT_SECRET_FILE=generic_client_secret_file -GENERIC_SCOPES=generic_scopes -GENERIC_AUTH_URL=generic_auth_url -GENERIC_TOKEN_URL=generic_token_url -GENERIC_USER_URL=generic_user_url -DISABLE_CONTINUE=false OAUTH_WHITELIST= GENERIC_NAME=My OAuth SESSION_EXPIRY=7200 @@ -30,4 +16,7 @@ OAUTH_AUTO_REDIRECT=none BACKGROUND_IMAGE=some_image_url GENERIC_SKIP_SSL=false RESOURCES_DIR=/data/resources -DATABASE_PATH=/data/tinyauth.db \ No newline at end of file +DATABASE_PATH=/data/tinyauth.db +DISABLE_ANALYTICS=false +DISABLE_RESOURCES=false +TRUSTED_PROXIES= \ No newline at end of file diff --git a/cmd/root.go b/cmd/root.go index c81a52a3..723cb368 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -95,6 +95,7 @@ func init() { {"database-path", "/data/tinyauth.db", "Path to the Sqlite database file."}, {"trusted-proxies", "", "Comma separated list of trusted proxies (IP addresses or CIDRs) for correct client IP detection."}, {"disable-analytics", false, "Disable anonymous version collection."}, + {"disable-resources", false, "Disable the resources server."}, } for _, opt := range configOptions { diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index 3416ff21..e92bb9a9 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -255,7 +255,8 @@ func (app *BootstrapApp) Setup() error { }, apiRouter, authService) resourcesController := controller.NewResourcesController(controller.ResourcesControllerConfig{ - ResourcesDir: app.config.ResourcesDir, + ResourcesDir: app.config.ResourcesDir, + ResourcesDisabled: app.config.DisableResources, }, mainRouter) healthController := controller.NewHealthController(apiRouter) diff --git a/internal/config/config.go b/internal/config/config.go index 32e586e7..28c14df6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -40,6 +40,7 @@ type Config struct { DatabasePath string `mapstructure:"database-path" validate:"required"` TrustedProxies string `mapstructure:"trusted-proxies"` DisableAnalytics bool `mapstructure:"disable-analytics"` + DisableResources bool `mapstructure:"disable-resources"` } // OAuth/OIDC config diff --git a/internal/controller/resources_controller.go b/internal/controller/resources_controller.go index 92384e75..bed4fcc7 100644 --- a/internal/controller/resources_controller.go +++ b/internal/controller/resources_controller.go @@ -7,7 +7,8 @@ import ( ) type ResourcesControllerConfig struct { - ResourcesDir string + ResourcesDir string + ResourcesDisabled bool } type ResourcesController struct { @@ -38,5 +39,12 @@ func (controller *ResourcesController) resourcesHandler(c *gin.Context) { }) return } + if controller.config.ResourcesDisabled { + c.JSON(403, gin.H{ + "status": 403, + "message": "Resources are disabled", + }) + return + } controller.fileServer.ServeHTTP(c.Writer, c.Request) } From e8558b89b4e46797f2a2103788cdb3515c1191ad Mon Sep 17 00:00:00 2001 From: Stavros Date: Mon, 22 Sep 2025 16:15:55 +0300 Subject: [PATCH 038/127] fix: set gin mode correctly --- internal/bootstrap/app_bootstrap.go | 8 ++++---- internal/service/oauth_broker_service.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index e92bb9a9..81a55ade 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -188,6 +188,10 @@ func (app *BootstrapApp) Setup() error { } // Create engine + if config.Version != "development" { + gin.SetMode(gin.ReleaseMode) + } + engine := gin.New() if len(app.config.TrustedProxies) > 0 { @@ -198,10 +202,6 @@ func (app *BootstrapApp) Setup() error { } } - if config.Version != "development" { - gin.SetMode(gin.ReleaseMode) - } - // Create middlewares var middlewares []Middleware diff --git a/internal/service/oauth_broker_service.go b/internal/service/oauth_broker_service.go index e6c6ddb3..178f9af9 100644 --- a/internal/service/oauth_broker_service.go +++ b/internal/service/oauth_broker_service.go @@ -50,7 +50,7 @@ func (broker *OAuthBrokerService) Init() error { log.Error().Err(err).Msgf("Failed to initialize OAuth service: %T", name) return err } - log.Info().Msgf("Initialized OAuth service: %T", name) + log.Info().Str("service", service.GetName()).Msg("Initialized OAuth service") } return nil From 5dd8526833f4f673a409d9588a58f11e739f97da Mon Sep 17 00:00:00 2001 From: Stavros Date: Mon, 22 Sep 2025 19:29:55 +0300 Subject: [PATCH 039/127] fix: fix key normalization function handing more cases than it needs to --- internal/utils/decoders/decoders.go | 14 +++++++++++--- internal/utils/decoders/decoders_test.go | 5 +++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/internal/utils/decoders/decoders.go b/internal/utils/decoders/decoders.go index 74937175..63604b14 100644 --- a/internal/utils/decoders/decoders.go +++ b/internal/utils/decoders/decoders.go @@ -18,10 +18,14 @@ func NormalizeKeys(keys map[string]string, rootName string, sep string) map[stri finalKey = append(finalKey, rootName) finalKey = append(finalKey, "providers") - kebabKey := strings.ToLower(k) + lowerKey := strings.ToLower(k) + + if !strings.HasPrefix(lowerKey, "providers"+sep) { + continue + } for _, known := range knownKeys { - if strings.HasSuffix(kebabKey, strings.ReplaceAll(known, "-", sep)) { + if strings.HasSuffix(lowerKey, strings.ReplaceAll(known, "-", sep)) { suffix = known break } @@ -31,7 +35,11 @@ func NormalizeKeys(keys map[string]string, rootName string, sep string) map[stri continue } - clientNameParts := strings.Split(strings.TrimPrefix(strings.TrimSuffix(kebabKey, sep+strings.ReplaceAll(suffix, "-", sep)), "providers"+sep), sep) + if strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(lowerKey, "providers"+sep), strings.ReplaceAll(suffix, "-", sep))) == "" { + continue + } + + clientNameParts := strings.Split(strings.TrimPrefix(strings.TrimSuffix(lowerKey, sep+strings.ReplaceAll(suffix, "-", sep)), "providers"+sep), sep) for i, p := range clientNameParts { if i == 0 { diff --git a/internal/utils/decoders/decoders_test.go b/internal/utils/decoders/decoders_test.go index 285760c9..fdec2869 100644 --- a/internal/utils/decoders/decoders_test.go +++ b/internal/utils/decoders/decoders_test.go @@ -14,6 +14,8 @@ func TestNormalizeKeys(t *testing.T) { "PROVIDERS_CLIENT1_CLIENT_SECRET": "my-client-secret", "PROVIDERS_MY_AWESOME_CLIENT_CLIENT_ID": "my-awesome-client-id", "PROVIDERS_MY_AWESOME_CLIENT_CLIENT_SECRET_FILE": "/path/to/secret", + "I_LOOK_LIKE_A_KEY_CLIENT_ID": "should-not-appear", + "PROVIDERS_CLIENT_ID": "should-not-appear", } expected := map[string]string{ "tinyauth.providers.client1.clientId": "my-client-id", @@ -31,6 +33,9 @@ func TestNormalizeKeys(t *testing.T) { "providers-client1-client-secret": "my-client-secret", "providers-my-awesome-client-client-id": "my-awesome-client-id", "providers-my-awesome-client-client-secret-file": "/path/to/secret", + "providers-should-not-appear-client": "should-not-appear", + "i-look-like-a-key-client-id": "should-not-appear", + "providers-client-id": "should-not-appear", } expected = map[string]string{ "tinyauth.providers.client1.clientId": "my-client-id", From c307f7eb2e787197c4b2cc2307f88a767f0fbccb Mon Sep 17 00:00:00 2001 From: Stavros Date: Mon, 22 Sep 2025 19:56:59 +0300 Subject: [PATCH 040/127] fix: handle 201 status for heartbeat --- internal/bootstrap/app_bootstrap.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index 81a55ade..dc31ac27 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -338,8 +338,8 @@ func (app *BootstrapApp) heartbeat() { res.Body.Close() - if res.StatusCode != 200 { - log.Debug().Str("status", res.Status).Msg("Heartbeat returned non-200 status") + if res.StatusCode != 200 && res.StatusCode != 201 { + log.Debug().Str("status", res.Status).Msg("Heartbeat returned non-200/201 status") } } } From 085f6257c53bdc0bd00a06aef70f10955a5145eb Mon Sep 17 00:00:00 2001 From: Stavros Date: Thu, 25 Sep 2025 22:35:44 +0300 Subject: [PATCH 041/127] fix: fix oauth group provider check --- internal/bootstrap/app_bootstrap.go | 6 +----- internal/config/config.go | 5 +++++ internal/service/auth_service.go | 10 ++++++---- internal/utils/app_utils.go | 11 +++++------ 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index dc31ac27..b9c38307 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -147,10 +147,6 @@ func (app *BootstrapApp) Setup() error { } // Configured providers - babysit := map[string]string{ - "google": "Google", - "github": "GitHub", - } configuredProviders := make([]controller.Provider, 0) for id, provider := range oauthProviders { @@ -159,7 +155,7 @@ func (app *BootstrapApp) Setup() error { } if provider.Name == "" { - if name, ok := babysit[id]; ok { + if name, ok := config.OverrideProviders[id]; ok { provider.Name = name } else { provider.Name = utils.Capitalize(id) diff --git a/internal/config/config.go b/internal/config/config.go index 28c14df6..23c38325 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -65,6 +65,11 @@ type OAuthServiceConfig struct { Name string `key:"name"` } +var OverrideProviders = map[string]string{ + "google": "Google", + "github": "GitHub", +} + // User/session related stuff type User struct { diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index 8925e491..4cd66b8a 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -309,12 +309,14 @@ func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserConte return true } - if context.Provider != "generic" { - log.Debug().Msg("Not using generic provider, skipping group check") - return true + for id := range config.OverrideProviders { + if context.Provider == id { + log.Info().Str("provider", id).Msg("OAuth groups not supported for this provider") + return true + } } - for _, userGroup := range strings.Split(context.OAuthGroups, ",") { + for userGroup := range strings.SplitSeq(context.OAuthGroups, ",") { if utils.CheckFilter(requiredGroups, strings.TrimSpace(userGroup)) { return true } diff --git a/internal/utils/app_utils.go b/internal/utils/app_utils.go index 643c9cf0..42649cd8 100644 --- a/internal/utils/app_utils.go +++ b/internal/utils/app_utils.go @@ -183,14 +183,13 @@ func GetOAuthProvidersConfig(env []string, args []string, appUrl string) (map[st providers[name] = provider } - // If we have google/github providers and no redirect URL babysit them - babysitProviders := []string{"google", "github"} + // If we have google/github providers and no redirect URL then set a default - for _, name := range babysitProviders { - if provider, exists := providers[name]; exists { + for id := range config.OverrideProviders { + if provider, exists := providers[id]; exists { if provider.RedirectURL == "" { - provider.RedirectURL = appUrl + "/api/oauth/callback/" + name - providers[name] = provider + provider.RedirectURL = appUrl + "/api/oauth/callback/" + id + providers[id] = provider } } } From a54996d72dc6d9ad11c2747b3181083f064381dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 16:32:17 +0300 Subject: [PATCH 042/127] chore(deps): bump oven/bun from 1.2.22-alpine to 1.2.23-alpine (#373) Bumps oven/bun from 1.2.22-alpine to 1.2.23-alpine. --- updated-dependencies: - dependency-name: oven/bun dependency-version: 1.2.23-alpine dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2bbe0581..a0ebc0d0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Site builder -FROM oven/bun:1.2.22-alpine AS frontend-builder +FROM oven/bun:1.2.23-alpine AS frontend-builder WORKDIR /frontend From d4069900bc66a328a5c8f21704f8e6624232a76c Mon Sep 17 00:00:00 2001 From: Stavros Date: Fri, 3 Oct 2025 16:36:02 +0300 Subject: [PATCH 043/127] feat: autofocus totp form --- frontend/src/components/auth/totp-form.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/auth/totp-form.tsx b/frontend/src/components/auth/totp-form.tsx index 9a8729b7..60e5dbdc 100644 --- a/frontend/src/components/auth/totp-form.tsx +++ b/frontend/src/components/auth/totp-form.tsx @@ -44,6 +44,7 @@ export const TotpForm = (props: Props) => { disabled={loading} {...field} autoComplete="one-time-code" + autoFocus > From dad07180913372f53ef0b95a40c30e6ce04a5635 Mon Sep 17 00:00:00 2001 From: Stavros Date: Sun, 5 Oct 2025 16:41:15 +0300 Subject: [PATCH 044/127] fix: disable search engine indexing --- frontend/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/index.html b/frontend/index.html index 718fe608..d8aea4a4 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -8,6 +8,7 @@ + Tinyauth From 2f1cb8dfe36ef0e9b21f710ad6ab7d4e503b68af Mon Sep 17 00:00:00 2001 From: Stavros Date: Sun, 5 Oct 2025 23:14:24 +0300 Subject: [PATCH 045/127] refactor: disable indexing completely in frontend --- frontend/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/index.html b/frontend/index.html index d8aea4a4..42e1f81e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -8,7 +8,7 @@ - + Tinyauth From 3ed180cb718b7a9b2d64b2cd3de9064a7b841c78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 5 Oct 2025 23:15:50 +0300 Subject: [PATCH 046/127] chore(deps): bump the minor-patch group across 1 directory with 21 updates (#379) Bumps the minor-patch group with 21 updates in the /frontend directory: | Package | From | To | | --- | --- | --- | | [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.1.13` | `4.1.14` | | [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.89.0` | `5.90.2` | | [i18next](https://github.com/i18next/i18next) | `25.5.2` | `25.5.3` | | [react](https://github.com/facebook/react/tree/HEAD/packages/react) | `19.1.1` | `19.2.0` | | [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.1.13` | `19.2.0` | | [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) | `19.1.1` | `19.2.0` | | [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `19.1.9` | `19.2.0` | | [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.62.0` | `7.63.0` | | [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) | `7.9.1` | `7.9.3` | | [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.1.13` | `4.1.14` | | [zod](https://github.com/colinhacks/zod) | `4.1.9` | `4.1.11` | | [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) | `9.35.0` | `9.36.0` | | [@tanstack/eslint-plugin-query](https://github.com/TanStack/query/tree/HEAD/packages/eslint-plugin-query) | `5.89.0` | `5.91.0` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `24.5.2` | `24.6.2` | | [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `5.0.3` | `5.0.4` | | [eslint](https://github.com/eslint/eslint) | `9.35.0` | `9.36.0` | | [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) | `0.4.20` | `0.4.23` | | [tw-animate-css](https://github.com/Wombosvideo/tw-animate-css) | `1.3.8` | `1.4.0` | | [typescript](https://github.com/microsoft/TypeScript) | `5.9.2` | `5.9.3` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.44.0` | `8.45.0` | | [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.1.6` | `7.1.8` | Updates `@tailwindcss/vite` from 4.1.13 to 4.1.14 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.14/packages/@tailwindcss-vite) Updates `@tanstack/react-query` from 5.89.0 to 5.90.2 - [Release notes](https://github.com/TanStack/query/releases) - [Commits](https://github.com/TanStack/query/commits/v5.90.2/packages/react-query) Updates `i18next` from 25.5.2 to 25.5.3 - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/i18next/compare/v25.5.2...v25.5.3) Updates `react` from 19.1.1 to 19.2.0 - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/v19.2.0/packages/react) Updates `@types/react` from 19.1.13 to 19.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react) Updates `react-dom` from 19.1.1 to 19.2.0 - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/v19.2.0/packages/react-dom) Updates `@types/react-dom` from 19.1.9 to 19.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom) Updates `react-hook-form` from 7.62.0 to 7.63.0 - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.62.0...v7.63.0) Updates `react-router` from 7.9.1 to 7.9.3 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router@7.9.3/packages/react-router) Updates `tailwindcss` from 4.1.13 to 4.1.14 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.14/packages/tailwindcss) Updates `zod` from 4.1.9 to 4.1.11 - [Release notes](https://github.com/colinhacks/zod/releases) - [Commits](https://github.com/colinhacks/zod/compare/v4.1.9...v4.1.11) Updates `@eslint/js` from 9.35.0 to 9.36.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/commits/v9.36.0/packages/js) Updates `@tanstack/eslint-plugin-query` from 5.89.0 to 5.91.0 - [Release notes](https://github.com/TanStack/query/releases) - [Changelog](https://github.com/TanStack/query/blob/main/packages/eslint-plugin-query/CHANGELOG.md) - [Commits](https://github.com/TanStack/query/commits/@tanstack/eslint-plugin-query@5.91.0/packages/eslint-plugin-query) Updates `@types/node` from 24.5.2 to 24.6.2 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/react` from 19.1.13 to 19.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react) Updates `@types/react-dom` from 19.1.9 to 19.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom) Updates `@vitejs/plugin-react` from 5.0.3 to 5.0.4 - [Release notes](https://github.com/vitejs/vite-plugin-react/releases) - [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@5.0.4/packages/plugin-react) Updates `eslint` from 9.35.0 to 9.36.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v9.35.0...v9.36.0) Updates `eslint-plugin-react-refresh` from 0.4.20 to 0.4.23 - [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases) - [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md) - [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.4.20...v0.4.23) Updates `tw-animate-css` from 1.3.8 to 1.4.0 - [Release notes](https://github.com/Wombosvideo/tw-animate-css/releases) - [Commits](https://github.com/Wombosvideo/tw-animate-css/compare/v1.3.8...v1.4.0) Updates `typescript` from 5.9.2 to 5.9.3 - [Release notes](https://github.com/microsoft/TypeScript/releases) - [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml) - [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.2...v5.9.3) Updates `typescript-eslint` from 8.44.0 to 8.45.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.45.0/packages/typescript-eslint) Updates `vite` from 7.1.6 to 7.1.8 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v7.1.8/packages/vite) --- updated-dependencies: - dependency-name: "@tailwindcss/vite" dependency-version: 4.1.14 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: "@tanstack/react-query" dependency-version: 5.90.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: i18next dependency-version: 25.5.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: react dependency-version: 19.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@types/react" dependency-version: 19.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: react-dom dependency-version: 19.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@types/react-dom" dependency-version: 19.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: react-hook-form dependency-version: 7.63.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: react-router dependency-version: 7.9.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: tailwindcss dependency-version: 4.1.14 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: zod dependency-version: 4.1.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: "@eslint/js" dependency-version: 9.36.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@tanstack/eslint-plugin-query" dependency-version: 5.91.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@types/node" dependency-version: 24.6.2 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@types/react" dependency-version: 19.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@types/react-dom" dependency-version: 19.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@vitejs/plugin-react" dependency-version: 5.0.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: eslint dependency-version: 9.36.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: eslint-plugin-react-refresh dependency-version: 0.4.23 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: tw-animate-css dependency-version: 1.4.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: typescript dependency-version: 5.9.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: typescript-eslint dependency-version: 8.45.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: vite dependency-version: 7.1.8 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/bun.lock | 204 ++++++++++++++++-------------------------- frontend/package.json | 42 ++++----- 2 files changed, 99 insertions(+), 147 deletions(-) diff --git a/frontend/bun.lock b/frontend/bun.lock index a190e7b4..cfa15db7 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -9,44 +9,44 @@ "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", - "@tailwindcss/vite": "^4.1.13", - "@tanstack/react-query": "^5.89.0", + "@tailwindcss/vite": "^4.1.14", + "@tanstack/react-query": "^5.90.2", "axios": "^1.12.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "i18next": "^25.5.2", + "i18next": "^25.5.3", "i18next-browser-languagedetector": "^8.2.0", "i18next-resources-to-backend": "^1.2.1", "input-otp": "^1.4.2", "lucide-react": "^0.544.0", "next-themes": "^0.4.6", - "react": "^19.1.1", - "react-dom": "^19.1.1", - "react-hook-form": "^7.62.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-hook-form": "^7.63.0", "react-i18next": "^15.7.3", "react-markdown": "^10.1.0", - "react-router": "^7.9.1", + "react-router": "^7.9.3", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", - "tailwindcss": "^4.1.13", - "zod": "^4.1.9", + "tailwindcss": "^4.1.14", + "zod": "^4.1.11", }, "devDependencies": { - "@eslint/js": "^9.35.0", - "@tanstack/eslint-plugin-query": "^5.89.0", - "@types/node": "^24.5.2", - "@types/react": "^19.1.13", - "@types/react-dom": "^19.1.9", - "@vitejs/plugin-react": "^5.0.3", - "eslint": "^9.35.0", + "@eslint/js": "^9.36.0", + "@tanstack/eslint-plugin-query": "^5.91.0", + "@types/node": "^24.6.2", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^5.0.4", + "eslint": "^9.36.0", "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.19", + "eslint-plugin-react-refresh": "^0.4.23", "globals": "^16.4.0", "prettier": "3.6.2", - "tw-animate-css": "^1.3.8", - "typescript": "~5.9.2", - "typescript-eslint": "^8.44.0", - "vite": "^7.1.6", + "tw-animate-css": "^1.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.45.0", + "vite": "^7.1.8", }, }, }, @@ -153,7 +153,7 @@ "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], - "@eslint/js": ["@eslint/js@9.35.0", "", {}, "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw=="], + "@eslint/js": ["@eslint/js@9.36.0", "", {}, "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw=="], "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], @@ -253,7 +253,7 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.35", "", {}, "sha512-slYrCpoxJUqzFDDNlvrOYRazQUNRvWPjXA17dAOISY3rDMxX6k8K4cj2H+hEYMHF81HO3uNd5rHVigAWRM5dSg=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.38", "", {}, "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.46.2", "", { "os": "android", "cpu": "arm" }, "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA=="], @@ -297,41 +297,41 @@ "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], - "@tailwindcss/node": ["@tailwindcss/node@4.1.13", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.5.1", "lightningcss": "1.30.1", "magic-string": "^0.30.18", "source-map-js": "^1.2.1", "tailwindcss": "4.1.13" } }, "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw=="], + "@tailwindcss/node": ["@tailwindcss/node@4.1.14", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.0", "lightningcss": "1.30.1", "magic-string": "^0.30.19", "source-map-js": "^1.2.1", "tailwindcss": "4.1.14" } }, "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw=="], - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.13", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.13", "@tailwindcss/oxide-darwin-arm64": "4.1.13", "@tailwindcss/oxide-darwin-x64": "4.1.13", "@tailwindcss/oxide-freebsd-x64": "4.1.13", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13", "@tailwindcss/oxide-linux-arm64-musl": "4.1.13", "@tailwindcss/oxide-linux-x64-gnu": "4.1.13", "@tailwindcss/oxide-linux-x64-musl": "4.1.13", "@tailwindcss/oxide-wasm32-wasi": "4.1.13", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13", "@tailwindcss/oxide-win32-x64-msvc": "4.1.13" } }, "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA=="], + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.14", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.5.1" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.14", "@tailwindcss/oxide-darwin-arm64": "4.1.14", "@tailwindcss/oxide-darwin-x64": "4.1.14", "@tailwindcss/oxide-freebsd-x64": "4.1.14", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.14", "@tailwindcss/oxide-linux-arm64-musl": "4.1.14", "@tailwindcss/oxide-linux-x64-gnu": "4.1.14", "@tailwindcss/oxide-linux-x64-musl": "4.1.14", "@tailwindcss/oxide-wasm32-wasi": "4.1.14", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.14", "@tailwindcss/oxide-win32-x64-msvc": "4.1.14" } }, "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw=="], - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.13", "", { "os": "android", "cpu": "arm64" }, "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew=="], + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.14", "", { "os": "android", "cpu": "arm64" }, "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ=="], - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ=="], + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA=="], - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw=="], + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw=="], - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.13", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ=="], + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.14", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw=="], - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13", "", { "os": "linux", "cpu": "arm" }, "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw=="], + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.14", "", { "os": "linux", "cpu": "arm" }, "sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw=="], - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ=="], + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w=="], - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg=="], + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ=="], - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.13", "", { "os": "linux", "cpu": "x64" }, "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ=="], + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.14", "", { "os": "linux", "cpu": "x64" }, "sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg=="], - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.13", "", { "os": "linux", "cpu": "x64" }, "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ=="], + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.14", "", { "os": "linux", "cpu": "x64" }, "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q=="], - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.13", "", { "dependencies": { "@emnapi/core": "^1.4.5", "@emnapi/runtime": "^1.4.5", "@emnapi/wasi-threads": "^1.0.4", "@napi-rs/wasm-runtime": "^0.2.12", "@tybys/wasm-util": "^0.10.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA=="], + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.14", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.5", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ=="], - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg=="], + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA=="], - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.13", "", { "os": "win32", "cpu": "x64" }, "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw=="], + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.14", "", { "os": "win32", "cpu": "x64" }, "sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA=="], - "@tailwindcss/vite": ["@tailwindcss/vite@4.1.13", "", { "dependencies": { "@tailwindcss/node": "4.1.13", "@tailwindcss/oxide": "4.1.13", "tailwindcss": "4.1.13" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ=="], + "@tailwindcss/vite": ["@tailwindcss/vite@4.1.14", "", { "dependencies": { "@tailwindcss/node": "4.1.14", "@tailwindcss/oxide": "4.1.14", "tailwindcss": "4.1.14" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-BoFUoU0XqgCUS1UXWhmDJroKKhNXeDzD7/XwabjkDIAbMnc4ULn5e2FuEuBbhZ6ENZoSYzKlzvZ44Yr6EUDUSA=="], - "@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.89.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.37.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-vz8TEuw9GO0xXIdreMpcofvOY17T3cjgob9bSFln8yQsKsbsUvtpvV3F8pVC3tZEDq0IwO++3/e0/+7YKEarNA=="], + "@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.91.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.44.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-Kn6yWyRe3dIPf7NqyDMhcsTBz2Oh8jPSOpBdlnLQhGBJ6iTMBFYA4B1UreGJ/WdfzQskSMh5imcyWF+wqa/Q5g=="], - "@tanstack/query-core": ["@tanstack/query-core@5.89.0", "", {}, "sha512-joFV1MuPhSLsKfTzwjmPDrp8ENfZ9N23ymFu07nLfn3JCkSHy0CFgsyhHTJOmWaumC/WiNIKM0EJyduCF/Ih/Q=="], + "@tanstack/query-core": ["@tanstack/query-core@5.90.2", "", {}, "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ=="], - "@tanstack/react-query": ["@tanstack/react-query@5.89.0", "", { "dependencies": { "@tanstack/query-core": "5.89.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-SXbtWSTSRXyBOe80mszPxpEbaN4XPRUp/i0EfQK1uyj3KCk/c8FuPJNIRwzOVe/OU3rzxrYtiNabsAmk1l714A=="], + "@tanstack/react-query": ["@tanstack/react-query@5.90.2", "", { "dependencies": { "@tanstack/query-core": "5.90.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -355,37 +355,37 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="], + "@types/node": ["@types/node@24.6.2", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang=="], - "@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="], + "@types/react": ["@types/react@19.2.0", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA=="], - "@types/react-dom": ["@types/react-dom@19.1.9", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ=="], + "@types/react-dom": ["@types/react-dom@19.2.0", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.44.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/type-utils": "8.44.0", "@typescript-eslint/utils": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.44.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-EGDAOGX+uwwekcS0iyxVDmRV9HX6FLSM5kzrAToLTsr9OWCIKG/y3lQheCq18yZ5Xh78rRKJiEpP0ZaCs4ryOQ=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.45.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/type-utils": "8.45.0", "@typescript-eslint/utils": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.45.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.44.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/types": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.45.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/types": "8.45.0", "@typescript-eslint/typescript-estree": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.44.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.44.0", "@typescript-eslint/types": "^8.44.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ZeaGNraRsq10GuEohKTo4295Z/SuGcSq2LzfGlqiuEvfArzo/VRrT0ZaJsVPuKZ55lVbNk8U6FcL+ZMH8CoyVA=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.45.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.45.0", "@typescript-eslint/types": "^8.45.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0" } }, "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.45.0", "", { "dependencies": { "@typescript-eslint/types": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0" } }, "sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.44.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-x5Y0+AuEPqAInc6yd0n5DAcvtoQ/vyaGwuX5HE9n6qAefk1GaedqrLQF8kQGylLUb9pnZyLf+iEiL9fr8APDtQ=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.45.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.44.0", "", { "dependencies": { "@typescript-eslint/types": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0", "@typescript-eslint/utils": "8.44.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-9cwsoSxJ8Sak67Be/hD2RNt/fsqmWnNE1iHohG8lxqLSNY8xNfyY7wloo5zpW3Nu9hxVgURevqfcH6vvKCt6yg=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.45.0", "", { "dependencies": { "@typescript-eslint/types": "8.45.0", "@typescript-eslint/typescript-estree": "8.45.0", "@typescript-eslint/utils": "8.45.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.45.0", "", {}, "sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.44.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.44.0", "@typescript-eslint/tsconfig-utils": "8.44.0", "@typescript-eslint/types": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-lqNj6SgnGcQZwL4/SBJ3xdPEfcBuhCG8zdcwCPgYcmiPLgokiNDKlbPzCwEwu7m279J/lBYWtDYL+87OEfn8Jw=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.45.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.45.0", "@typescript-eslint/tsconfig-utils": "8.45.0", "@typescript-eslint/types": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.43.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/types": "8.43.0", "@typescript-eslint/typescript-estree": "8.43.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.45.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/types": "8.45.0", "@typescript-eslint/typescript-estree": "8.45.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.44.0", "", { "dependencies": { "@typescript-eslint/types": "8.44.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-zaz9u8EJ4GBmnehlrpoKvj/E3dNbuQ7q0ucyZImm3cLqJ8INTc970B1qEqDX/Rzq65r3TvVTN7kHWPBoyW7DWw=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.45.0", "", { "dependencies": { "@typescript-eslint/types": "8.45.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - "@vitejs/plugin-react": ["@vitejs/plugin-react@5.0.3", "", { "dependencies": { "@babel/core": "^7.28.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.35", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-PFVHhosKkofGH0Yzrw1BipSedTH68BFF8ZWy1kfUpCtJcouXXY0+racG8sExw7hw0HoX36813ga5o3LTWZ4FUg=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.0.4", "", { "dependencies": { "@babel/core": "^7.28.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.38", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], @@ -491,11 +491,11 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@9.35.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.35.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg=="], + "eslint": ["eslint@9.36.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.36.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ=="], "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], - "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.20", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA=="], + "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.23", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-G4j+rv0NmbIR45kni5xJOrYvCtyD3/7LjpVH8MPPcudXDcNu8gv+4ATTDXTtbRR8rTCM5HxECvCSsRmxKnWDsA=="], "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], @@ -579,7 +579,7 @@ "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], - "i18next": ["i18next@25.5.2", "", { "dependencies": { "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-lW8Zeh37i/o0zVr+NoCHfNnfvVw+M6FQbRp36ZZ/NyHDJ3NJVpp2HhAUyU9WafL5AssymNoOjMRB48mmx2P6Hw=="], + "i18next": ["i18next@25.5.3", "", { "dependencies": { "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-joFqorDeQ6YpIXni944upwnuHBf5IoPMuqAchGVeQLdWC2JOjxgM9V8UGLhNIIH/Q8QleRxIi0BSRQehSrDLcg=="], "i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.0", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g=="], @@ -739,9 +739,7 @@ "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - "minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="], - - "mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -785,11 +783,11 @@ "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - "react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="], + "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], - "react-dom": ["react-dom@19.1.1", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.1" } }, "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw=="], + "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], - "react-hook-form": ["react-hook-form@7.62.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA=="], + "react-hook-form": ["react-hook-form@7.63.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA=="], "react-i18next": ["react-i18next@15.7.3", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 25.4.1", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-AANws4tOE+QSq/IeMF/ncoHlMNZaVLxpa5uUGW1wjike68elVYr0018L9xYoqBr1OFO7G7boDPrbn0HpMCJxTw=="], @@ -801,7 +799,7 @@ "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], - "react-router": ["react-router@7.9.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-pfAByjcTpX55mqSDGwGnY9vDCpxqBLASg0BMNAuMmpSGESo/TaOUG6BllhAtAkCGx8Rnohik/XtaqiYUJtgW2g=="], + "react-router": ["react-router@7.9.3", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-4o2iWCFIwhI/eYAIL43+cjORXYn/aRQPgtFRRZb3VzoyQ5Uej0Bmqj7437L97N9NJW4wnicSwLOLS+yCXfAPgg=="], "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], @@ -817,7 +815,7 @@ "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -845,11 +843,11 @@ "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="], - "tailwindcss": ["tailwindcss@4.1.13", "", {}, "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w=="], + "tailwindcss": ["tailwindcss@4.1.14", "", {}, "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA=="], "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], - "tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="], + "tar": ["tar@7.5.1", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], @@ -863,15 +861,15 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "tw-animate-css": ["tw-animate-css@1.3.8", "", {}, "sha512-Qrk3PZ7l7wUcGYhwZloqfkWCmaXZAoqjkdbIDvzfGshwGtexa/DAs9koXxIkrpEasyevandomzCBAV1Yyop5rw=="], + "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "typescript-eslint": ["typescript-eslint@8.44.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.44.0", "@typescript-eslint/parser": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0", "@typescript-eslint/utils": "8.44.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ib7mCkYuIzYonCq9XWF5XNw+fkj2zg629PSa9KNIQ47RXFF763S5BIX4wqz1+FLPogTZoiw8KmCiRPRa8bL3qw=="], + "typescript-eslint": ["typescript-eslint@8.45.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.45.0", "@typescript-eslint/parser": "8.45.0", "@typescript-eslint/typescript-estree": "8.45.0", "@typescript-eslint/utils": "8.45.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qzDmZw/Z5beNLUrXfd0HIW6MzIaAV5WNDxmMs9/3ojGOpYavofgNAAD/nC6tGV2PczIi0iw8vot2eAe/sBn7zg=="], - "undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="], + "undici-types": ["undici-types@7.13.0", "", {}, "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ=="], "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], @@ -897,7 +895,7 @@ "vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="], - "vite": ["vite@7.1.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-SRYIB8t/isTwNn8vMB3MR6E+EQZM/WG1aKmmIUCfDXfVvKfc20ZpamngWHKzAmmu9ppsgxsg4b2I7c90JZudIQ=="], + "vite": ["vite@7.1.8", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-oBXvfSHEOL8jF+R9Am7h59Up07kVVGH1NrFGFoEG6bPDZP3tGpQhvkBpy5x7U6+E6wZCu9OihsWgJqDbQIm8LQ=="], "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], @@ -909,7 +907,7 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "zod": ["zod@4.1.9", "", {}, "sha512-HI32jTq0AUAC125z30E8bQNz0RQ+9Uc+4J7V97gLYjZVKRjeydPgGt6dvQzFrav7MYOUGFqqOGiHpA/fdbd0cQ=="], + "zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], @@ -939,17 +937,17 @@ "@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], - "@tailwindcss/node/jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="], + "@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" }, "bundled": true }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.4", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" }, "bundled": true }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.5", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-TBr9Cf9onSAS2LQ2+QHx6XcC6h9+RIzJgbqG3++9TUZSH204AwEy5jg3BTQ0VATsyoGj4ee49tN/y6rvaOOtcg=="], - "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="], + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -967,34 +965,12 @@ "@types/estree-jsx/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], - "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.44.0", "", { "dependencies": { "@typescript-eslint/types": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0" } }, "sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA=="], - - "@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.44.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/types": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg=="], - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.4", "", {}, "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="], - "@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.44.0", "", { "dependencies": { "@typescript-eslint/types": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0" } }, "sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA=="], - - "@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="], - - "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="], - - "@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw=="], - - "@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="], - - "@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.44.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/types": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg=="], - - "@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="], - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], - "@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.43.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.43.0", "@typescript-eslint/tsconfig-utils": "8.43.0", "@typescript-eslint/types": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw=="], - - "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="], - "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "hast-util-to-jsx-runtime/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], @@ -1009,8 +985,6 @@ "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - "typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.44.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/types": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg=="], - "@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.27.1", "", { "dependencies": { "@babel/parser": "^7.27.1", "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w=="], "@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.27.2", "", { "dependencies": { "@babel/types": "^7.27.1" }, "bin": "./bin/babel-parser.js" }, "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw=="], @@ -1025,34 +999,12 @@ "@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], - "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="], - - "@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="], - - "@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.44.0", "", { "dependencies": { "@typescript-eslint/types": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0" } }, "sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.43.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.43.0", "@typescript-eslint/types": "^8.43.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw=="], - - "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.43.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA=="], - - "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw=="], - - "@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], - - "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.44.0", "", { "dependencies": { "@typescript-eslint/types": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0" } }, "sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA=="], - - "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="], - "@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], "@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], - "@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - "@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], "@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], diff --git a/frontend/package.json b/frontend/package.json index 211dea4b..4d7b4597 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,43 +15,43 @@ "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", - "@tailwindcss/vite": "^4.1.13", - "@tanstack/react-query": "^5.89.0", + "@tailwindcss/vite": "^4.1.14", + "@tanstack/react-query": "^5.90.2", "axios": "^1.12.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "i18next": "^25.5.2", + "i18next": "^25.5.3", "i18next-browser-languagedetector": "^8.2.0", "i18next-resources-to-backend": "^1.2.1", "input-otp": "^1.4.2", "lucide-react": "^0.544.0", "next-themes": "^0.4.6", - "react": "^19.1.1", - "react-dom": "^19.1.1", - "react-hook-form": "^7.62.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-hook-form": "^7.63.0", "react-i18next": "^15.7.3", "react-markdown": "^10.1.0", - "react-router": "^7.9.1", + "react-router": "^7.9.3", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", - "tailwindcss": "^4.1.13", - "zod": "^4.1.9" + "tailwindcss": "^4.1.14", + "zod": "^4.1.11" }, "devDependencies": { - "@eslint/js": "^9.35.0", - "@tanstack/eslint-plugin-query": "^5.89.0", - "@types/node": "^24.5.2", - "@types/react": "^19.1.13", - "@types/react-dom": "^19.1.9", - "@vitejs/plugin-react": "^5.0.3", - "eslint": "^9.35.0", + "@eslint/js": "^9.36.0", + "@tanstack/eslint-plugin-query": "^5.91.0", + "@types/node": "^24.6.2", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^5.0.4", + "eslint": "^9.36.0", "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.19", + "eslint-plugin-react-refresh": "^0.4.23", "globals": "^16.4.0", "prettier": "3.6.2", - "tw-animate-css": "^1.3.8", - "typescript": "~5.9.2", - "typescript-eslint": "^8.44.0", - "vite": "^7.1.6" + "tw-animate-css": "^1.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.45.0", + "vite": "^7.1.8" } } \ No newline at end of file From 121c629d51e605279b8852e53f66769576339502 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 5 Oct 2025 23:17:33 +0300 Subject: [PATCH 047/127] chore(deps): bump the minor-patch group across 1 directory with 3 updates (#380) Bumps the minor-patch group with 3 updates in the / directory: [github.com/gin-gonic/gin](https://github.com/gin-gonic/gin), [github.com/docker/docker](https://github.com/docker/docker) and [github.com/go-ldap/ldap/v3](https://github.com/go-ldap/ldap). Updates `github.com/gin-gonic/gin` from 1.10.1 to 1.11.0 - [Release notes](https://github.com/gin-gonic/gin/releases) - [Changelog](https://github.com/gin-gonic/gin/blob/master/CHANGELOG.md) - [Commits](https://github.com/gin-gonic/gin/compare/v1.10.1...v1.11.0) Updates `github.com/docker/docker` from 28.4.0+incompatible to 28.5.0+incompatible - [Release notes](https://github.com/docker/docker/releases) - [Commits](https://github.com/docker/docker/compare/v28.4.0...v28.5.0) Updates `github.com/go-ldap/ldap/v3` from 3.4.11 to 3.4.12 - [Release notes](https://github.com/go-ldap/ldap/releases) - [Commits](https://github.com/go-ldap/ldap/compare/v3.4.11...v3.4.12) --- updated-dependencies: - dependency-name: github.com/gin-gonic/gin dependency-version: 1.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: github.com/docker/docker dependency-version: 28.5.0+incompatible dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: github.com/go-ldap/ldap/v3 dependency-version: 3.4.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 29 +++++++++++++++----------- go.sum | 65 +++++++++++++++++++++++++++++----------------------------- 2 files changed, 49 insertions(+), 45 deletions(-) diff --git a/go.mod b/go.mod index 7ec10480..74ad6fb1 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.24.3 require ( github.com/cenkalti/backoff/v5 v5.0.3 - github.com/gin-gonic/gin v1.10.1 + github.com/gin-gonic/gin v1.11.0 github.com/glebarez/sqlite v1.11.0 github.com/go-playground/validator/v10 v10.27.0 github.com/golang-migrate/migrate/v4 v4.19.0 @@ -34,6 +34,7 @@ require ( github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -43,13 +44,18 @@ require ( github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect go.opentelemetry.io/otel/sdk v1.34.0 // indirect + go.uber.org/mock v0.5.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/mod v0.27.0 // indirect golang.org/x/term v0.35.0 // indirect + golang.org/x/tools v0.36.0 // indirect modernc.org/libc v1.66.3 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect @@ -62,8 +68,8 @@ require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/boombuler/barcode v1.0.2 // indirect - github.com/bytedance/sonic v1.12.7 // indirect - github.com/bytedance/sonic/loader v0.2.3 // indirect + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/catppuccin/go v0.3.0 // indirect github.com/charmbracelet/bubbles v0.21.0 // indirect github.com/charmbracelet/bubbletea v1.3.4 // indirect @@ -72,9 +78,9 @@ require ( github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect - github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/docker v28.4.0+incompatible + github.com/docker/docker v28.5.0+incompatible 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 @@ -82,8 +88,8 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect - github.com/gin-contrib/sse v1.0.0 // indirect - github.com/go-ldap/ldap/v3 v3.4.11 + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-ldap/ldap/v3 v3.4.12 github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -91,7 +97,7 @@ require ( github.com/goccy/go-json v0.10.4 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect @@ -118,17 +124,16 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // 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.3.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect go.opentelemetry.io/otel v1.37.0 // indirect go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect - golang.org/x/arch v0.13.0 // indirect + golang.org/x/arch v0.20.0 // indirect golang.org/x/net v0.44.0 // indirect golang.org/x/oauth2 v0.31.0 golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.36.0 // indirect golang.org/x/text v0.29.0 // indirect - google.golang.org/protobuf v1.36.3 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + google.golang.org/protobuf v1.36.9 // indirect ) diff --git a/go.sum b/go.sum index 1f40013f..de35e1b7 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= -github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI= +github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= @@ -17,11 +17,10 @@ github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/ github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4= github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q= -github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= -github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -56,9 +55,8 @@ github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8 github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= -github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= -github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= @@ -74,8 +72,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/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 v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk= -github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.5.0+incompatible h1:ZdSQoRUE9XxhFI/B8YLvhnEFMmYN9Pp8Egd2qcaFk1E= +github.com/docker/docker v28.5.0+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= @@ -92,18 +90,18 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= -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-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= -github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= -github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU= -github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM= +github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4= +github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -121,6 +119,8 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE= github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0= @@ -163,10 +163,8 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 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/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/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -229,6 +227,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -260,14 +262,10 @@ github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjb github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -276,8 +274,8 @@ github.com/traefik/paerser v0.2.2 h1:cpzW/ZrQrBh3mdwD/jnp6aXASiUFKOVr6ldP+keJTcQ github.com/traefik/paerser v0.2.2/go.mod h1:7BBDd4FANoVgaTZG+yh26jI6CA2nds7D/4VTEdIsh24= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 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/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/weppos/publicsuffix-go v0.50.0 h1:M178k6l8cnh9T1c1cStkhytVxdk5zPd6gGZf8ySIuVo= github.com/weppos/publicsuffix-go v0.50.0/go.mod h1:VXhClBYMlDrUsome4pOTpe68Ui0p6iQRAbyHQD1yKoU= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= @@ -300,10 +298,12 @@ go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mx go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -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.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= @@ -338,8 +338,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= -google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= -google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -376,6 +376,5 @@ modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= From 30fe695371ccb1e32b7f54543436cc35375cef84 Mon Sep 17 00:00:00 2001 From: Stavros Date: Sun, 5 Oct 2025 23:19:59 +0300 Subject: [PATCH 048/127] New Crowdin updates (#278) * New translations en.json (French) * New translations en.json (Russian) * New translations en.json (Chinese Simplified) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Swedish) * New translations en.json (Swedish) * New translations en.json (Russian) * New translations en.json (Czech) * New translations en.json (German) * New translations en.json (German) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (Portuguese, Brazilian) * New translations en.json (French) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Greek) * New translations en.json (Polish) * New translations en.json (Arabic) * New translations en.json (French) * New translations en.json (French) * New translations en.json (Chinese Simplified) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Russian) * New translations en.json (Russian) --- frontend/src/lib/i18n/locales/af-ZA.json | 19 ++-- frontend/src/lib/i18n/locales/ar-SA.json | 19 ++-- frontend/src/lib/i18n/locales/ca-ES.json | 19 ++-- frontend/src/lib/i18n/locales/cs-CZ.json | 115 ++++++++++++----------- frontend/src/lib/i18n/locales/da-DK.json | 19 ++-- frontend/src/lib/i18n/locales/de-DE.json | 25 +++-- frontend/src/lib/i18n/locales/el-GR.json | 19 ++-- frontend/src/lib/i18n/locales/es-ES.json | 19 ++-- frontend/src/lib/i18n/locales/fi-FI.json | 19 ++-- frontend/src/lib/i18n/locales/fr-FR.json | 23 +++-- frontend/src/lib/i18n/locales/he-IL.json | 19 ++-- frontend/src/lib/i18n/locales/hu-HU.json | 19 ++-- frontend/src/lib/i18n/locales/it-IT.json | 19 ++-- frontend/src/lib/i18n/locales/ja-JP.json | 19 ++-- frontend/src/lib/i18n/locales/ko-KR.json | 19 ++-- frontend/src/lib/i18n/locales/nl-NL.json | 19 ++-- frontend/src/lib/i18n/locales/no-NO.json | 19 ++-- frontend/src/lib/i18n/locales/pl-PL.json | 19 ++-- frontend/src/lib/i18n/locales/pt-BR.json | 53 ++++++----- frontend/src/lib/i18n/locales/pt-PT.json | 19 ++-- frontend/src/lib/i18n/locales/ro-RO.json | 19 ++-- frontend/src/lib/i18n/locales/ru-RU.json | 45 +++++---- frontend/src/lib/i18n/locales/sr-SP.json | 19 ++-- frontend/src/lib/i18n/locales/sv-SE.json | 69 +++++++------- frontend/src/lib/i18n/locales/tr-TR.json | 19 ++-- frontend/src/lib/i18n/locales/uk-UA.json | 21 +++-- frontend/src/lib/i18n/locales/vi-VN.json | 19 ++-- frontend/src/lib/i18n/locales/zh-CN.json | 27 +++--- frontend/src/lib/i18n/locales/zh-TW.json | 19 ++-- 29 files changed, 461 insertions(+), 316 deletions(-) diff --git a/frontend/src/lib/i18n/locales/af-ZA.json b/frontend/src/lib/i18n/locales/af-ZA.json index 74e422f5..43004285 100644 --- a/frontend/src/lib/i18n/locales/af-ZA.json +++ b/frontend/src/lib/i18n/locales/af-ZA.json @@ -14,14 +14,17 @@ "loginOauthFailSubtitle": "Failed to get OAuth URL", "loginOauthSuccessTitle": "Redirecting", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", + "continueTitle": "Continue", "continueRedirectingTitle": "Redirecting...", "continueRedirectingSubtitle": "You should be redirected to the app soon", - "continueInvalidRedirectTitle": "Invalid redirect", - "continueInvalidRedirectSubtitle": "The redirect URL is invalid", + "continueRedirectManually": "Redirect me manually", "continueInsecureRedirectTitle": "Insecure redirect", "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", - "continueTitle": "Continue", - "continueSubtitle": "Click the button to continue to your app.", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", "logoutFailTitle": "Failed to log out", "logoutFailSubtitle": "Please try again", "logoutSuccessTitle": "Logged out", @@ -44,8 +47,6 @@ "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", "unauthorizedButton": "Try again", - "untrustedRedirectTitle": "Untrusted redirect", - "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", "cancelTitle": "Cancel", "forgotPasswordTitle": "Forgot your password?", "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", @@ -53,5 +54,9 @@ "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.", "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "invalidInput": "Invalid input", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/ar-SA.json b/frontend/src/lib/i18n/locales/ar-SA.json index b5cc2109..98c620c5 100644 --- a/frontend/src/lib/i18n/locales/ar-SA.json +++ b/frontend/src/lib/i18n/locales/ar-SA.json @@ -14,14 +14,17 @@ "loginOauthFailSubtitle": "أخفق الحصول على رابط OAuth", "loginOauthSuccessTitle": "إعادة توجيه", "loginOauthSuccessSubtitle": "إعادة توجيه إلى مزود OAuth الخاص بك", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", + "continueTitle": "متابعة", "continueRedirectingTitle": "إعادة توجيه...", "continueRedirectingSubtitle": "يجب إعادة توجيهك إلى التطبيق قريبا", - "continueInvalidRedirectTitle": "إعادة توجيه غير صالحة", - "continueInvalidRedirectSubtitle": "رابط إعادة التوجيه غير صالح", + "continueRedirectManually": "Redirect me manually", "continueInsecureRedirectTitle": "إعادة توجيه غير آمنة", "continueInsecureRedirectSubtitle": "أنت تحاول إعادة التوجيه من https إلى http، هل أنت متأكد أنك تريد المتابعة؟", - "continueTitle": "متابعة", - "continueSubtitle": "انقر الزر للمتابعة إلى التطبيق الخاص بك.", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", "logoutFailTitle": "فشل تسجيل الخروج", "logoutFailSubtitle": "يرجى إعادة المحاولة", "logoutSuccessTitle": "تم تسجيل الخروج", @@ -44,8 +47,6 @@ "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", "unauthorizedButton": "حاول مجددا", - "untrustedRedirectTitle": "إعادة توجيه غير موثوقة", - "untrustedRedirectSubtitle": "أنت تحاول إعادة التوجيه إلى نطاق لا يتطابق مع النطاق المكون الخاص بك ({{domain}}). هل أنت متأكد من أنك تريد المتابعة؟", "cancelTitle": "إلغاء", "forgotPasswordTitle": "نسيت كلمة المرور؟", "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", @@ -53,5 +54,9 @@ "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.", "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "invalidInput": "Invalid input", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.", + "ignoreTitle": "تجاهل", + "goToCorrectDomainTitle": "Go to correct domain" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/ca-ES.json b/frontend/src/lib/i18n/locales/ca-ES.json index 74e422f5..43004285 100644 --- a/frontend/src/lib/i18n/locales/ca-ES.json +++ b/frontend/src/lib/i18n/locales/ca-ES.json @@ -14,14 +14,17 @@ "loginOauthFailSubtitle": "Failed to get OAuth URL", "loginOauthSuccessTitle": "Redirecting", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", + "continueTitle": "Continue", "continueRedirectingTitle": "Redirecting...", "continueRedirectingSubtitle": "You should be redirected to the app soon", - "continueInvalidRedirectTitle": "Invalid redirect", - "continueInvalidRedirectSubtitle": "The redirect URL is invalid", + "continueRedirectManually": "Redirect me manually", "continueInsecureRedirectTitle": "Insecure redirect", "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", - "continueTitle": "Continue", - "continueSubtitle": "Click the button to continue to your app.", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", "logoutFailTitle": "Failed to log out", "logoutFailSubtitle": "Please try again", "logoutSuccessTitle": "Logged out", @@ -44,8 +47,6 @@ "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", "unauthorizedButton": "Try again", - "untrustedRedirectTitle": "Untrusted redirect", - "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", "cancelTitle": "Cancel", "forgotPasswordTitle": "Forgot your password?", "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", @@ -53,5 +54,9 @@ "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.", "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "invalidInput": "Invalid input", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/cs-CZ.json b/frontend/src/lib/i18n/locales/cs-CZ.json index 74e422f5..0308327c 100644 --- a/frontend/src/lib/i18n/locales/cs-CZ.json +++ b/frontend/src/lib/i18n/locales/cs-CZ.json @@ -1,57 +1,62 @@ { - "loginTitle": "Welcome back, login with", - "loginTitleSimple": "Welcome back, please login", - "loginDivider": "Or", - "loginUsername": "Username", - "loginPassword": "Password", - "loginSubmit": "Login", - "loginFailTitle": "Failed to log in", - "loginFailSubtitle": "Please check your username and password", - "loginFailRateLimit": "You failed to login too many times. Please try again later", - "loginSuccessTitle": "Logged in", - "loginSuccessSubtitle": "Welcome back!", - "loginOauthFailTitle": "An error occurred", - "loginOauthFailSubtitle": "Failed to get OAuth URL", - "loginOauthSuccessTitle": "Redirecting", - "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", - "continueRedirectingTitle": "Redirecting...", - "continueRedirectingSubtitle": "You should be redirected to the app soon", - "continueInvalidRedirectTitle": "Invalid redirect", - "continueInvalidRedirectSubtitle": "The redirect URL is invalid", - "continueInsecureRedirectTitle": "Insecure redirect", - "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", - "continueTitle": "Continue", - "continueSubtitle": "Click the button to continue to your app.", - "logoutFailTitle": "Failed to log out", - "logoutFailSubtitle": "Please try again", - "logoutSuccessTitle": "Logged out", - "logoutSuccessSubtitle": "You have been logged out", - "logoutTitle": "Logout", - "logoutUsernameSubtitle": "You are currently logged in as {{username}}. Click the button below to logout.", - "logoutOauthSubtitle": "You are currently logged in as {{username}} using the {{provider}} OAuth provider. Click the button below to logout.", - "notFoundTitle": "Page not found", - "notFoundSubtitle": "The page you are looking for does not exist.", - "notFoundButton": "Go home", - "totpFailTitle": "Failed to verify code", - "totpFailSubtitle": "Please check your code and try again", - "totpSuccessTitle": "Verified", - "totpSuccessSubtitle": "Redirecting to your app", - "totpTitle": "Enter your TOTP code", - "totpSubtitle": "Please enter the code from your authenticator app.", - "unauthorizedTitle": "Unauthorized", - "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", - "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", - "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", - "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", - "unauthorizedButton": "Try again", - "untrustedRedirectTitle": "Untrusted redirect", - "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel", - "forgotPasswordTitle": "Forgot your password?", - "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", - "errorTitle": "An error occurred", - "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.", - "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", - "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "loginTitle": "Vítejte zpět, přihlaste se pomocí", + "loginTitleSimple": "Vítejte zpět, přihlaste se prosím", + "loginDivider": "Nebo", + "loginUsername": "Uživatelské jméno", + "loginPassword": "Heslo", + "loginSubmit": "Přihlásit", + "loginFailTitle": "Přihlášení se nezdařilo", + "loginFailSubtitle": "Zkontrolujte prosím své uživatelské jméno a heslo", + "loginFailRateLimit": "Přiliš mnoho neúspěšných pokusů přihlášení. Zkuste to prosím později", + "loginSuccessTitle": "Přihlášen", + "loginSuccessSubtitle": "Vítejte zpět!", + "loginOauthFailTitle": "Došlo k chybě", + "loginOauthFailSubtitle": "Nepodařilo se získat OAuth URL", + "loginOauthSuccessTitle": "Přesměrování", + "loginOauthSuccessSubtitle": "Přesměrování k poskytovateli OAuth", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", + "continueTitle": "Pokračovat", + "continueRedirectingTitle": "Přesměrování...", + "continueRedirectingSubtitle": "Brzy budete přesměrováni do aplikace", + "continueRedirectManually": "Redirect me manually", + "continueInsecureRedirectTitle": "Nezabezpečené přesměrování", + "continueInsecureRedirectSubtitle": "Pokoušíte se přesměrovat z https na http, které není bezpečné. Opravdu chcete pokračovat?", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", + "logoutFailTitle": "Odhlášení se nezdařilo", + "logoutFailSubtitle": "Zkuste to prosím znovu", + "logoutSuccessTitle": "Odhlášen", + "logoutSuccessSubtitle": "Byl jste odhlášen", + "logoutTitle": "Odhlásit", + "logoutUsernameSubtitle": "Jste přihlášen jako {{username}}. Pro odhlášení klikněte na tlačítko níže.", + "logoutOauthSubtitle": "Jste přihlášen jako {{username}} pomocí {{provider}} poskytovatele OAuth. Pro odhlášení klikněte na tlačítko níže.", + "notFoundTitle": "Stránka nenalezena", + "notFoundSubtitle": "Stránka, kterou hledáte, neexistuje.", + "notFoundButton": "Jít domů", + "totpFailTitle": "Nepodařilo se ověřit kód", + "totpFailSubtitle": "Zkontrolujte prosím kód a zkuste to znovu", + "totpSuccessTitle": "Ověřeno", + "totpSuccessSubtitle": "Přesměrování do aplikace", + "totpTitle": "Zadejte TOTP kód", + "totpSubtitle": "Zadejte prosím kód z ověřovací aplikace.", + "unauthorizedTitle": "Nepovoleno", + "unauthorizedResourceSubtitle": "Uživatel s uživatelským jménem {{username}} není oprávněn k přístupu ke zdroji {{resource}}.", + "unauthorizedLoginSubtitle": "Uživatel s uživatelským jménem {{username}} není oprávněn k přihlášení.", + "unauthorizedGroupsSubtitle": "Uživatel s uživatelským jménem {{username}} není ve skupině potřebné k přístupu ke zdroji {{resource}}.", + "unauthorizedIpSubtitle": "Vaše IP adresa {{ip}} není oprávněna k přístupu ke zdroji {{resource}}.", + "unauthorizedButton": "Zkusit znovu", + "cancelTitle": "Zrušit", + "forgotPasswordTitle": "Zapomněli jste heslo?", + "failedToFetchProvidersTitle": "Nepodařilo se načíst poskytovatele ověřování. Zkontrolujte prosím konfiguraci.", + "errorTitle": "Došlo k chybě", + "errorSubtitle": "Nastala chyba při pokusu o provedení této akce. Pro více informací prosím zkontrolujte konzolu.", + "forgotPasswordMessage": "Heslo můžete obnovit změnou proměnné `USERS`.", + "fieldRequired": "Toto pole je povinné", + "invalidInput": "Neplatný údaj", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/da-DK.json b/frontend/src/lib/i18n/locales/da-DK.json index 95f58178..c3801975 100644 --- a/frontend/src/lib/i18n/locales/da-DK.json +++ b/frontend/src/lib/i18n/locales/da-DK.json @@ -14,14 +14,17 @@ "loginOauthFailSubtitle": "Kunne ikke hente OAuth-URL", "loginOauthSuccessTitle": "Omdirigerer", "loginOauthSuccessSubtitle": "Omdirigerer til din OAuth-udbyder", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", + "continueTitle": "Fortsæt", "continueRedirectingTitle": "Omdirigerer...", "continueRedirectingSubtitle": "Du bør blive omdirigeret til appen snart", - "continueInvalidRedirectTitle": "Ugyldig omdirigering", - "continueInvalidRedirectSubtitle": "Omdirigerings-URL'en er ugyldig", + "continueRedirectManually": "Redirect me manually", "continueInsecureRedirectTitle": "Usikker omdirigering", "continueInsecureRedirectSubtitle": "Du forsøger at omdirigere fra https til http, som ikke er sikker. Er du sikker på, at du vil fortsætte?", - "continueTitle": "Fortsæt", - "continueSubtitle": "Klik på knappen for at fortsætte til din app.", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", "logoutFailTitle": "Log ud mislykkedes", "logoutFailSubtitle": "Prøv venligst igen", "logoutSuccessTitle": "Logget ud", @@ -44,8 +47,6 @@ "unauthorizedGroupsSubtitle": "Brugeren med brugernavnet {{username}} er ikke i de grupper, som ressourcen {{resource}} kræver.", "unauthorizedIpSubtitle": "Din IP adresse {{ip}} er ikke autoriseret til at tilgå ressourcen {{resource}}.", "unauthorizedButton": "Prøv igen", - "untrustedRedirectTitle": "Usikker omdirigering", - "untrustedRedirectSubtitle": "Du forsøger at omdirigere til et domæne, der ikke matcher dit konfigurerede domæne ({{domain}}). Er du sikker på, at du vil fortsætte?", "cancelTitle": "Annuller", "forgotPasswordTitle": "Glemt din adgangskode?", "failedToFetchProvidersTitle": "Kunne ikke indlæse godkendelsesudbydere. Tjek venligst din konfiguration.", @@ -53,5 +54,9 @@ "errorSubtitle": "Der opstod en fejl under forsøget på at udføre denne handling. Tjek venligst konsollen for mere information.", "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "invalidInput": "Invalid input", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/de-DE.json b/frontend/src/lib/i18n/locales/de-DE.json index 4c55082b..709767c4 100644 --- a/frontend/src/lib/i18n/locales/de-DE.json +++ b/frontend/src/lib/i18n/locales/de-DE.json @@ -14,14 +14,17 @@ "loginOauthFailSubtitle": "Fehler beim Abrufen der OAuth-URL", "loginOauthSuccessTitle": "Leite weiter", "loginOauthSuccessSubtitle": "Weiterleitung zu Ihrem OAuth-Provider", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", + "continueTitle": "Weiter", "continueRedirectingTitle": "Leite weiter...", "continueRedirectingSubtitle": "Sie sollten in Kürze zur App weitergeleitet werden", - "continueInvalidRedirectTitle": "Ungültige Weiterleitung", - "continueInvalidRedirectSubtitle": "Die Weiterleitungs-URL ist ungültig", + "continueRedirectManually": "Redirect me manually", "continueInsecureRedirectTitle": "Unsichere Weiterleitung", "continueInsecureRedirectSubtitle": "Sie versuchen von https auf http weiterzuleiten, was unsicher ist. Sind Sie sicher, dass Sie fortfahren möchten?", - "continueTitle": "Weiter", - "continueSubtitle": "Klicken Sie auf den Button, um zur App zu gelangen.", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", "logoutFailTitle": "Abmelden fehlgeschlagen", "logoutFailSubtitle": "Bitte versuchen Sie es erneut", "logoutSuccessTitle": "Abgemeldet", @@ -31,7 +34,7 @@ "logoutOauthSubtitle": "Sie sind derzeit als {{username}} über den OAuth-Anbieter {{provider}} angemeldet. Klicken Sie auf den Button unten, um sich abzumelden.", "notFoundTitle": "Seite nicht gefunden", "notFoundSubtitle": "Die gesuchte Seite existiert nicht.", - "notFoundButton": "Nach Hause", + "notFoundButton": "Zurück", "totpFailTitle": "Fehler beim Verifizieren des Codes", "totpFailSubtitle": "Bitte überprüfen Sie Ihren Code und versuchen Sie es erneut", "totpSuccessTitle": "Verifiziert", @@ -44,14 +47,16 @@ "unauthorizedGroupsSubtitle": "Der Benutzer mit Benutzername {{username}} ist nicht in den Gruppen, die von der Ressource {{resource}} benötigt werden.", "unauthorizedIpSubtitle": "Ihre IP-Adresse {{ip}} ist nicht berechtigt, auf die Ressource {{resource}} zuzugreifen.", "unauthorizedButton": "Erneut versuchen", - "untrustedRedirectTitle": "Nicht vertrauenswürdige Weiterleitung", - "untrustedRedirectSubtitle": "Sie versuchen auf eine Domain umzuleiten, die nicht mit Ihrer konfigurierten Domain übereinstimmt ({{domain}}). Sind Sie sicher, dass Sie fortfahren möchten?", "cancelTitle": "Abbrechen", "forgotPasswordTitle": "Passwort vergessen?", "failedToFetchProvidersTitle": "Fehler beim Laden der Authentifizierungsanbieter. Bitte überprüfen Sie Ihre Konfiguration.", "errorTitle": "Ein Fehler ist aufgetreten", "errorSubtitle": "Beim Versuch, diese Aktion auszuführen, ist ein Fehler aufgetreten. Bitte überprüfen Sie die Konsole für weitere Informationen.", - "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", - "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "forgotPasswordMessage": "Das Passwort kann durch Änderung der 'USERS' Variable zurückgesetzt werden.", + "fieldRequired": "Dieses Feld ist notwendig", + "invalidInput": "Ungültige Eingabe", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/el-GR.json b/frontend/src/lib/i18n/locales/el-GR.json index 5cc5ceb4..ee401526 100644 --- a/frontend/src/lib/i18n/locales/el-GR.json +++ b/frontend/src/lib/i18n/locales/el-GR.json @@ -14,14 +14,17 @@ "loginOauthFailSubtitle": "Αποτυχία λήψης OAuth URL", "loginOauthSuccessTitle": "Ανακατεύθυνση", "loginOauthSuccessSubtitle": "Ανακατεύθυνση στον πάροχο OAuth σας", + "loginOauthAutoRedirectTitle": "Αυτόματη Ανακατεύθυνση OAuth", + "loginOauthAutoRedirectSubtitle": "Θα ανακατευθυνθείτε αυτόματα στον πάροχο OAuth σας για να επαληθευτείτε.", + "loginOauthAutoRedirectButton": "Ανακατεύθυνση τώρα", + "continueTitle": "Συνέχεια", "continueRedirectingTitle": "Ανακατεύθυνση...", "continueRedirectingSubtitle": "Θα πρέπει να μεταφερθείτε σύντομα στην εφαρμογή σας", - "continueInvalidRedirectTitle": "Μη έγκυρη ανακατεύθυνση", - "continueInvalidRedirectSubtitle": "Το URL ανακατεύθυνσης δεν είναι έγκυρο", + "continueRedirectManually": "Χειροκίνητη ανακατεύθυνση", "continueInsecureRedirectTitle": "Μη ασφαλής ανακατεύθυνση", "continueInsecureRedirectSubtitle": "Προσπαθείτε να ανακατευθύνετε από https σε http το οποίο δεν είναι ασφαλές. Είστε σίγουροι ότι θέλετε να συνεχίσετε;", - "continueTitle": "Συνέχεια", - "continueSubtitle": "Κάντε κλικ στο κουμπί για να συνεχίσετε στην εφαρμογή σας.", + "continueUntrustedRedirectTitle": "Μη έμπιστη ανακατεύθυνση", + "continueUntrustedRedirectSubtitle": "Προσπαθείτε να ανακατευθύνετε σε ένα domain που δεν ταιριάζει με το ρυθμισμένο domain σας ({{cookieDomain}}). Είστε βέβαιοι ότι θέλετε να συνεχίσετε;", "logoutFailTitle": "Αποτυχία αποσύνδεσης", "logoutFailSubtitle": "Παρακαλώ δοκιμάστε ξανά", "logoutSuccessTitle": "Αποσυνδεδεμένος", @@ -44,8 +47,6 @@ "unauthorizedGroupsSubtitle": "Ο χρήστης με όνομα χρήστη {{username}} δεν είναι στις ομάδες που απαιτούνται από τον πόρο {{resource}}.", "unauthorizedIpSubtitle": "Η διεύθυνση IP σας {{ip}} δεν είναι εξουσιοδοτημένη να έχει πρόσβαση στον πόρο {{resource}}.", "unauthorizedButton": "Προσπαθήστε ξανά", - "untrustedRedirectTitle": "Μη έμπιστη ανακατεύθυνση", - "untrustedRedirectSubtitle": "Προσπαθείτε να ανακατευθύνετε σε ένα domain που δεν ταιριάζει με τον ρυθμισμένο domain σας ({{domain}}). Είστε βέβαιοι ότι θέλετε να συνεχίσετε;", "cancelTitle": "Ακύρωση", "forgotPasswordTitle": "Ξεχάσατε το συνθηματικό σας;", "failedToFetchProvidersTitle": "Αποτυχία φόρτωσης παρόχων πιστοποίησης. Παρακαλώ ελέγξτε τις ρυθμίσεις σας.", @@ -53,5 +54,9 @@ "errorSubtitle": "Παρουσιάστηκε σφάλμα κατά την προσπάθεια εκτέλεσης αυτής της ενέργειας. Ελέγξτε την κονσόλα για περισσότερες πληροφορίες.", "forgotPasswordMessage": "Μπορείτε να επαναφέρετε τον κωδικό πρόσβασής σας αλλάζοντας τη μεταβλητή περιβάλλοντος `USERS`.", "fieldRequired": "Αυτό το πεδίο είναι υποχρεωτικό", - "invalidInput": "Μη έγκυρη καταχώρηση" + "invalidInput": "Μη έγκυρη καταχώρηση", + "domainWarningTitle": "Μη έγκυρο domain", + "domainWarningSubtitle": "Αυτή η εφαρμογή έχει ρυθμιστεί για πρόσβαση από {{appUrl}}, αλλά {{currentUrl}} χρησιμοποιείται. Αν συνεχίσετε, μπορεί να αντιμετωπίσετε προβλήματα με την ταυτοποίηση.", + "ignoreTitle": "Παράβλεψη", + "goToCorrectDomainTitle": "Μεταβείτε στο σωστό domain" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/es-ES.json b/frontend/src/lib/i18n/locales/es-ES.json index 40b043ff..a4500b30 100644 --- a/frontend/src/lib/i18n/locales/es-ES.json +++ b/frontend/src/lib/i18n/locales/es-ES.json @@ -14,14 +14,17 @@ "loginOauthFailSubtitle": "Error al obtener la URL de OAuth", "loginOauthSuccessTitle": "Redireccionando", "loginOauthSuccessSubtitle": "Redireccionando a tu proveedor de OAuth", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", + "continueTitle": "Continuar", "continueRedirectingTitle": "Redireccionando...", "continueRedirectingSubtitle": "Pronto será redirigido a la aplicación", - "continueInvalidRedirectTitle": "Redirección inválida", - "continueInvalidRedirectSubtitle": "La URL de redirección es inválida", + "continueRedirectManually": "Redirect me manually", "continueInsecureRedirectTitle": "Redirección insegura", "continueInsecureRedirectSubtitle": "Está intentando redirigir desde https a http lo cual no es seguro. ¿Está seguro que desea continuar?", - "continueTitle": "Continuar", - "continueSubtitle": "Haga clic en el botón para continuar hacia su aplicación.", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", "logoutFailTitle": "Fallo al cerrar sesión", "logoutFailSubtitle": "Por favor intente nuevamente", "logoutSuccessTitle": "Sesión cerrada", @@ -44,8 +47,6 @@ "unauthorizedGroupsSubtitle": "El usuario con nombre de usuario {{username}} no está en los grupos requeridos por el recurso {{resource}}.", "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", "unauthorizedButton": "Inténtelo de nuevo", - "untrustedRedirectTitle": "Redirección no confiable", - "untrustedRedirectSubtitle": "Está intentando redirigir a un dominio que no coincide con su dominio configurado ({{domain}}). ¿Está seguro que desea continuar?", "cancelTitle": "Cancelar", "forgotPasswordTitle": "¿Olvidó su contraseña?", "failedToFetchProvidersTitle": "Error al cargar los proveedores de autenticación. Por favor revise su configuración.", @@ -53,5 +54,9 @@ "errorSubtitle": "Ocurrió un error mientras se trataba de realizar esta acción. Por favor, revise la consola para más información.", "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "invalidInput": "Invalid input", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/fi-FI.json b/frontend/src/lib/i18n/locales/fi-FI.json index 74e422f5..43004285 100644 --- a/frontend/src/lib/i18n/locales/fi-FI.json +++ b/frontend/src/lib/i18n/locales/fi-FI.json @@ -14,14 +14,17 @@ "loginOauthFailSubtitle": "Failed to get OAuth URL", "loginOauthSuccessTitle": "Redirecting", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", + "continueTitle": "Continue", "continueRedirectingTitle": "Redirecting...", "continueRedirectingSubtitle": "You should be redirected to the app soon", - "continueInvalidRedirectTitle": "Invalid redirect", - "continueInvalidRedirectSubtitle": "The redirect URL is invalid", + "continueRedirectManually": "Redirect me manually", "continueInsecureRedirectTitle": "Insecure redirect", "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", - "continueTitle": "Continue", - "continueSubtitle": "Click the button to continue to your app.", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", "logoutFailTitle": "Failed to log out", "logoutFailSubtitle": "Please try again", "logoutSuccessTitle": "Logged out", @@ -44,8 +47,6 @@ "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", "unauthorizedButton": "Try again", - "untrustedRedirectTitle": "Untrusted redirect", - "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", "cancelTitle": "Cancel", "forgotPasswordTitle": "Forgot your password?", "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", @@ -53,5 +54,9 @@ "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.", "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "invalidInput": "Invalid input", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/fr-FR.json b/frontend/src/lib/i18n/locales/fr-FR.json index ffd2b925..69c32e74 100644 --- a/frontend/src/lib/i18n/locales/fr-FR.json +++ b/frontend/src/lib/i18n/locales/fr-FR.json @@ -14,14 +14,17 @@ "loginOauthFailSubtitle": "Impossible d'obtenir l'URL OAuth", "loginOauthSuccessTitle": "Redirection", "loginOauthSuccessSubtitle": "Redirection vers votre fournisseur OAuth", + "loginOauthAutoRedirectTitle": "Redirection automatique OAuth", + "loginOauthAutoRedirectSubtitle": "Vous allez être automatiquement redirigé vers votre fournisseur OAuth pour vous authentifier.", + "loginOauthAutoRedirectButton": "Rediriger", + "continueTitle": "Continuer", "continueRedirectingTitle": "Redirection...", "continueRedirectingSubtitle": "Vous devriez être redirigé vers l'application bientôt", - "continueInvalidRedirectTitle": "Redirection invalide", - "continueInvalidRedirectSubtitle": "L'URL de redirection est invalide", + "continueRedirectManually": "Redirection manuelle", "continueInsecureRedirectTitle": "Redirection non sécurisée", "continueInsecureRedirectSubtitle": "Vous tentez de rediriger de https vers http, ce qui n'est pas sécurisé. Êtes-vous sûr de vouloir continuer ?", - "continueTitle": "Continuer", - "continueSubtitle": "Cliquez sur le bouton pour continuer vers votre application.", + "continueUntrustedRedirectTitle": "Redirection non sécurisée", + "continueUntrustedRedirectSubtitle": "Vous essayez de rediriger vers un domaine qui ne correspond pas à votre domaine configuré ({{cookieDomain}}). Êtes-vous sûr de vouloir continuer ?", "logoutFailTitle": "Échec de la déconnexion", "logoutFailSubtitle": "Veuillez réessayer", "logoutSuccessTitle": "Déconnecté", @@ -38,20 +41,22 @@ "totpSuccessSubtitle": "Redirection vers votre application", "totpTitle": "Saisissez votre code TOTP", "totpSubtitle": "Veuillez saisir le code de votre application d'authentification.", - "unauthorizedTitle": "Unauthorized", + "unauthorizedTitle": "Non autorisé", "unauthorizedResourceSubtitle": "L'utilisateur avec le nom d'utilisateur {{username}} n'est pas autorisé à accéder à la ressource {{resource}}.", "unauthorizedLoginSubtitle": "L'utilisateur avec le nom d'utilisateur {{username}} n'est pas autorisé à se connecter.", "unauthorizedGroupsSubtitle": "L'utilisateur avec le nom d'utilisateur {{username}} n'appartient pas aux groupes requis par la ressource {{resource}}.", "unauthorizedIpSubtitle": "Votre adresse IP {{ip}} n'est pas autorisée à accéder à la ressource {{resource}}.", "unauthorizedButton": "Réessayer", - "untrustedRedirectTitle": "Redirection non fiable", - "untrustedRedirectSubtitle": "Vous tentez de rediriger vers un domaine qui ne correspond pas à votre domaine configuré ({{domain}}). Êtes-vous sûr de vouloir continuer ?", "cancelTitle": "Annuler", "forgotPasswordTitle": "Mot de passe oublié ?", "failedToFetchProvidersTitle": "Échec du chargement des fournisseurs d'authentification. Veuillez vérifier votre configuration.", "errorTitle": "Une erreur est survenue", "errorSubtitle": "Une erreur est survenue lors de l'exécution de cette action. Veuillez consulter la console pour plus d'informations.", "forgotPasswordMessage": "Vous pouvez réinitialiser votre mot de passe en modifiant la variable d'environnement `USERS`.", - "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "fieldRequired": "Ce champ est obligatoire", + "invalidInput": "Saisie non valide", + "domainWarningTitle": "Domaine invalide", + "domainWarningSubtitle": "Cette instance est configurée pour être accédée depuis {{appUrl}}, mais {{currentUrl}} est utilisé. Si vous continuez, vous pourriez rencontrer des problèmes d'authentification.", + "ignoreTitle": "Ignorer", + "goToCorrectDomainTitle": "Aller au bon domaine" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/he-IL.json b/frontend/src/lib/i18n/locales/he-IL.json index 74e422f5..43004285 100644 --- a/frontend/src/lib/i18n/locales/he-IL.json +++ b/frontend/src/lib/i18n/locales/he-IL.json @@ -14,14 +14,17 @@ "loginOauthFailSubtitle": "Failed to get OAuth URL", "loginOauthSuccessTitle": "Redirecting", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", + "continueTitle": "Continue", "continueRedirectingTitle": "Redirecting...", "continueRedirectingSubtitle": "You should be redirected to the app soon", - "continueInvalidRedirectTitle": "Invalid redirect", - "continueInvalidRedirectSubtitle": "The redirect URL is invalid", + "continueRedirectManually": "Redirect me manually", "continueInsecureRedirectTitle": "Insecure redirect", "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", - "continueTitle": "Continue", - "continueSubtitle": "Click the button to continue to your app.", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", "logoutFailTitle": "Failed to log out", "logoutFailSubtitle": "Please try again", "logoutSuccessTitle": "Logged out", @@ -44,8 +47,6 @@ "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", "unauthorizedButton": "Try again", - "untrustedRedirectTitle": "Untrusted redirect", - "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", "cancelTitle": "Cancel", "forgotPasswordTitle": "Forgot your password?", "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", @@ -53,5 +54,9 @@ "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.", "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "invalidInput": "Invalid input", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/hu-HU.json b/frontend/src/lib/i18n/locales/hu-HU.json index 74e422f5..43004285 100644 --- a/frontend/src/lib/i18n/locales/hu-HU.json +++ b/frontend/src/lib/i18n/locales/hu-HU.json @@ -14,14 +14,17 @@ "loginOauthFailSubtitle": "Failed to get OAuth URL", "loginOauthSuccessTitle": "Redirecting", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", + "continueTitle": "Continue", "continueRedirectingTitle": "Redirecting...", "continueRedirectingSubtitle": "You should be redirected to the app soon", - "continueInvalidRedirectTitle": "Invalid redirect", - "continueInvalidRedirectSubtitle": "The redirect URL is invalid", + "continueRedirectManually": "Redirect me manually", "continueInsecureRedirectTitle": "Insecure redirect", "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", - "continueTitle": "Continue", - "continueSubtitle": "Click the button to continue to your app.", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", "logoutFailTitle": "Failed to log out", "logoutFailSubtitle": "Please try again", "logoutSuccessTitle": "Logged out", @@ -44,8 +47,6 @@ "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", "unauthorizedButton": "Try again", - "untrustedRedirectTitle": "Untrusted redirect", - "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", "cancelTitle": "Cancel", "forgotPasswordTitle": "Forgot your password?", "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", @@ -53,5 +54,9 @@ "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.", "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "invalidInput": "Invalid input", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/it-IT.json b/frontend/src/lib/i18n/locales/it-IT.json index 74e422f5..43004285 100644 --- a/frontend/src/lib/i18n/locales/it-IT.json +++ b/frontend/src/lib/i18n/locales/it-IT.json @@ -14,14 +14,17 @@ "loginOauthFailSubtitle": "Failed to get OAuth URL", "loginOauthSuccessTitle": "Redirecting", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", + "continueTitle": "Continue", "continueRedirectingTitle": "Redirecting...", "continueRedirectingSubtitle": "You should be redirected to the app soon", - "continueInvalidRedirectTitle": "Invalid redirect", - "continueInvalidRedirectSubtitle": "The redirect URL is invalid", + "continueRedirectManually": "Redirect me manually", "continueInsecureRedirectTitle": "Insecure redirect", "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", - "continueTitle": "Continue", - "continueSubtitle": "Click the button to continue to your app.", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", "logoutFailTitle": "Failed to log out", "logoutFailSubtitle": "Please try again", "logoutSuccessTitle": "Logged out", @@ -44,8 +47,6 @@ "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", "unauthorizedButton": "Try again", - "untrustedRedirectTitle": "Untrusted redirect", - "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", "cancelTitle": "Cancel", "forgotPasswordTitle": "Forgot your password?", "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", @@ -53,5 +54,9 @@ "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.", "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "invalidInput": "Invalid input", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/ja-JP.json b/frontend/src/lib/i18n/locales/ja-JP.json index 74e422f5..43004285 100644 --- a/frontend/src/lib/i18n/locales/ja-JP.json +++ b/frontend/src/lib/i18n/locales/ja-JP.json @@ -14,14 +14,17 @@ "loginOauthFailSubtitle": "Failed to get OAuth URL", "loginOauthSuccessTitle": "Redirecting", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", + "continueTitle": "Continue", "continueRedirectingTitle": "Redirecting...", "continueRedirectingSubtitle": "You should be redirected to the app soon", - "continueInvalidRedirectTitle": "Invalid redirect", - "continueInvalidRedirectSubtitle": "The redirect URL is invalid", + "continueRedirectManually": "Redirect me manually", "continueInsecureRedirectTitle": "Insecure redirect", "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", - "continueTitle": "Continue", - "continueSubtitle": "Click the button to continue to your app.", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", "logoutFailTitle": "Failed to log out", "logoutFailSubtitle": "Please try again", "logoutSuccessTitle": "Logged out", @@ -44,8 +47,6 @@ "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", "unauthorizedButton": "Try again", - "untrustedRedirectTitle": "Untrusted redirect", - "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", "cancelTitle": "Cancel", "forgotPasswordTitle": "Forgot your password?", "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", @@ -53,5 +54,9 @@ "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.", "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "invalidInput": "Invalid input", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/ko-KR.json b/frontend/src/lib/i18n/locales/ko-KR.json index 74e422f5..43004285 100644 --- a/frontend/src/lib/i18n/locales/ko-KR.json +++ b/frontend/src/lib/i18n/locales/ko-KR.json @@ -14,14 +14,17 @@ "loginOauthFailSubtitle": "Failed to get OAuth URL", "loginOauthSuccessTitle": "Redirecting", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", + "continueTitle": "Continue", "continueRedirectingTitle": "Redirecting...", "continueRedirectingSubtitle": "You should be redirected to the app soon", - "continueInvalidRedirectTitle": "Invalid redirect", - "continueInvalidRedirectSubtitle": "The redirect URL is invalid", + "continueRedirectManually": "Redirect me manually", "continueInsecureRedirectTitle": "Insecure redirect", "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", - "continueTitle": "Continue", - "continueSubtitle": "Click the button to continue to your app.", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", "logoutFailTitle": "Failed to log out", "logoutFailSubtitle": "Please try again", "logoutSuccessTitle": "Logged out", @@ -44,8 +47,6 @@ "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", "unauthorizedButton": "Try again", - "untrustedRedirectTitle": "Untrusted redirect", - "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", "cancelTitle": "Cancel", "forgotPasswordTitle": "Forgot your password?", "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", @@ -53,5 +54,9 @@ "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.", "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "invalidInput": "Invalid input", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/nl-NL.json b/frontend/src/lib/i18n/locales/nl-NL.json index 8a1bac34..3e890486 100644 --- a/frontend/src/lib/i18n/locales/nl-NL.json +++ b/frontend/src/lib/i18n/locales/nl-NL.json @@ -14,14 +14,17 @@ "loginOauthFailSubtitle": "Fout bij het ophalen van OAuth URL", "loginOauthSuccessTitle": "Omleiden", "loginOauthSuccessSubtitle": "Omleiden naar je OAuth provider", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", + "continueTitle": "Ga verder", "continueRedirectingTitle": "Omleiden...", "continueRedirectingSubtitle": "Je wordt naar de app doorgestuurd", - "continueInvalidRedirectTitle": "Ongeldige omleiding", - "continueInvalidRedirectSubtitle": "De omleidings-URL is ongeldig", + "continueRedirectManually": "Redirect me manually", "continueInsecureRedirectTitle": "Onveilige doorverwijzing", "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", - "continueTitle": "Ga verder", - "continueSubtitle": "Klik op de knop om door te gaan naar de app.", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", "logoutFailTitle": "Afmelden mislukt", "logoutFailSubtitle": "Probeer het opnieuw", "logoutSuccessTitle": "Afgemeld", @@ -44,8 +47,6 @@ "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", "unauthorizedButton": "Opnieuw proberen", - "untrustedRedirectTitle": "Untrusted redirect", - "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", "cancelTitle": "Cancel", "forgotPasswordTitle": "Forgot your password?", "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", @@ -53,5 +54,9 @@ "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.", "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "invalidInput": "Invalid input", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/no-NO.json b/frontend/src/lib/i18n/locales/no-NO.json index 74e422f5..43004285 100644 --- a/frontend/src/lib/i18n/locales/no-NO.json +++ b/frontend/src/lib/i18n/locales/no-NO.json @@ -14,14 +14,17 @@ "loginOauthFailSubtitle": "Failed to get OAuth URL", "loginOauthSuccessTitle": "Redirecting", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", + "continueTitle": "Continue", "continueRedirectingTitle": "Redirecting...", "continueRedirectingSubtitle": "You should be redirected to the app soon", - "continueInvalidRedirectTitle": "Invalid redirect", - "continueInvalidRedirectSubtitle": "The redirect URL is invalid", + "continueRedirectManually": "Redirect me manually", "continueInsecureRedirectTitle": "Insecure redirect", "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", - "continueTitle": "Continue", - "continueSubtitle": "Click the button to continue to your app.", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", "logoutFailTitle": "Failed to log out", "logoutFailSubtitle": "Please try again", "logoutSuccessTitle": "Logged out", @@ -44,8 +47,6 @@ "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", "unauthorizedButton": "Try again", - "untrustedRedirectTitle": "Untrusted redirect", - "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", "cancelTitle": "Cancel", "forgotPasswordTitle": "Forgot your password?", "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", @@ -53,5 +54,9 @@ "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.", "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "invalidInput": "Invalid input", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/pl-PL.json b/frontend/src/lib/i18n/locales/pl-PL.json index 37b68168..abded9b4 100644 --- a/frontend/src/lib/i18n/locales/pl-PL.json +++ b/frontend/src/lib/i18n/locales/pl-PL.json @@ -14,14 +14,17 @@ "loginOauthFailSubtitle": "Nie udało się uzyskać adresu URL OAuth", "loginOauthSuccessTitle": "Przekierowywanie", "loginOauthSuccessSubtitle": "Przekierowywanie do Twojego dostawcy OAuth", + "loginOauthAutoRedirectTitle": "Automatyczne przekierowanie OAuth", + "loginOauthAutoRedirectSubtitle": "Nastąpi automatyczne przekierowanie do dostawcy OAuth w celu uwierzytelnienia.", + "loginOauthAutoRedirectButton": "Przekieruj teraz", + "continueTitle": "Kontynuuj", "continueRedirectingTitle": "Przekierowywanie...", "continueRedirectingSubtitle": "Wkrótce powinieneś zostać przekierowany do aplikacji", - "continueInvalidRedirectTitle": "Nieprawidłowe przekierowanie", - "continueInvalidRedirectSubtitle": "Adres przekierowania jest nieprawidłowy", + "continueRedirectManually": "Przekieruj mnie ręcznie", "continueInsecureRedirectTitle": "Niezabezpieczone przekierowanie", "continueInsecureRedirectSubtitle": "Próbujesz przekierować z https do http, co nie jest bezpieczne. Czy na pewno chcesz kontynuować?", - "continueTitle": "Kontynuuj", - "continueSubtitle": "Kliknij przycisk, aby przejść do aplikacji.", + "continueUntrustedRedirectTitle": "Niezaufane przekierowanie", + "continueUntrustedRedirectSubtitle": "Próbujesz przekierować do domeny, która nie pasuje do skonfigurowanej domeny ({{cookieDomain}}). Czy na pewno chcesz kontynuować?", "logoutFailTitle": "Nie udało się wylogować", "logoutFailSubtitle": "Spróbuj ponownie", "logoutSuccessTitle": "Wylogowano", @@ -44,8 +47,6 @@ "unauthorizedGroupsSubtitle": "Użytkownik o nazwie {{username}} nie należy do grup wymaganych przez zasób {{resource}}.", "unauthorizedIpSubtitle": "Twój adres IP {{ip}} nie ma autoryzacji do dostępu do zasobu {{resource}}.", "unauthorizedButton": "Spróbuj ponownie", - "untrustedRedirectTitle": "Niezaufane przekierowanie", - "untrustedRedirectSubtitle": "Próbujesz przekierować do domeny, która nie pasuje do Twojej skonfigurowanej domeny ({{domain}}). Czy na pewno chcesz kontynuować?", "cancelTitle": "Anuluj", "forgotPasswordTitle": "Nie pamiętasz hasła?", "failedToFetchProvidersTitle": "Nie udało się załadować dostawców uwierzytelniania. Sprawdź swoją konfigurację.", @@ -53,5 +54,9 @@ "errorSubtitle": "Wystąpił błąd podczas próby wykonania tej czynności. Sprawdź konsolę, aby uzyskać więcej informacji.", "forgotPasswordMessage": "Możesz zresetować hasło, zmieniając zmienną środowiskową `USERS`.", "fieldRequired": "To pole jest wymagane", - "invalidInput": "Nieprawidłowe dane wejściowe" + "invalidInput": "Nieprawidłowe dane wejściowe", + "domainWarningTitle": "Nieprawidłowa domena", + "domainWarningSubtitle": "Ta instancja jest skonfigurowana do uzyskania dostępu z {{appUrl}}, ale {{currentUrl}} jest w użyciu. Jeśli będziesz kontynuować, mogą wystąpić problemy z uwierzytelnianiem.", + "ignoreTitle": "Zignoruj", + "goToCorrectDomainTitle": "Przejdź do prawidłowej domeny" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/pt-BR.json b/frontend/src/lib/i18n/locales/pt-BR.json index 1a2626cd..c33e8c8d 100644 --- a/frontend/src/lib/i18n/locales/pt-BR.json +++ b/frontend/src/lib/i18n/locales/pt-BR.json @@ -1,34 +1,37 @@ { "loginTitle": "Bem-vindo de volta, acesse com", - "loginTitleSimple": "Welcome back, please login", - "loginDivider": "Or", + "loginTitleSimple": "Bem-vindo de volta, faça o login", + "loginDivider": "Ou", "loginUsername": "Nome de usuário", "loginPassword": "Senha", "loginSubmit": "Entrar", "loginFailTitle": "Falha ao iniciar sessão", "loginFailSubtitle": "Por favor, verifique seu usuário e senha", - "loginFailRateLimit": "You failed to login too many times. Please try again later", + "loginFailRateLimit": "Você falhou em iniciar sessão muitas vezes, por favor tente novamente mais tarde", "loginSuccessTitle": "Sessão Iniciada", "loginSuccessSubtitle": "Bem-vindo de volta!", - "loginOauthFailTitle": "An error occurred", + "loginOauthFailTitle": "Ocorreu um erro", "loginOauthFailSubtitle": "Falha ao obter URL de OAuth", "loginOauthSuccessTitle": "Redirecionando", "loginOauthSuccessSubtitle": "Redirecionando para seu provedor OAuth", + "loginOauthAutoRedirectTitle": "Redirecionamento automático do OAuth", + "loginOauthAutoRedirectSubtitle": "Você será automaticamente redirecionado para seu provedor OAuth para autenticar.", + "loginOauthAutoRedirectButton": "Redirecionar agora", + "continueTitle": "Continuar", "continueRedirectingTitle": "Redirecionando...", "continueRedirectingSubtitle": "Você deve ser redirecionado para o aplicativo em breve", - "continueInvalidRedirectTitle": "Redirecionamento inválido", - "continueInvalidRedirectSubtitle": "O endereço de redirecionamento é inválido", + "continueRedirectManually": "Redirecionar-me manualmente", "continueInsecureRedirectTitle": "Redirecionamento inseguro", - "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", - "continueTitle": "Continuar", - "continueSubtitle": "Clique no botão para continuar para o seu aplicativo.", + "continueInsecureRedirectSubtitle": "Você está tentando redirecionar de https para http, você tem certeza que deseja continuar?", + "continueUntrustedRedirectTitle": "Redirecionamento não confiável", + "continueUntrustedRedirectSubtitle": "Você está tentando redirecionar para um domínio que não corresponde ao seu domínio configurado ({{cookieDomain}}). Tem certeza que deseja continuar?", "logoutFailTitle": "Falha ao encerrar sessão", "logoutFailSubtitle": "Por favor, tente novamente", "logoutSuccessTitle": "Sessão encerrada", "logoutSuccessSubtitle": "Você foi desconectado", "logoutTitle": "Sair", - "logoutUsernameSubtitle": "You are currently logged in as {{username}}. Click the button below to logout.", - "logoutOauthSubtitle": "You are currently logged in as {{username}} using the {{provider}} OAuth provider. Click the button below to logout.", + "logoutUsernameSubtitle": "Você está atualmente logado como {{username}}, clique no botão abaixo para sair.", + "logoutOauthSubtitle": "Você está atualmente logado como {{username}} usando o provedor {{provider}} OAuth, clique no botão abaixo para sair.", "notFoundTitle": "Página não encontrada", "notFoundSubtitle": "A página que você está procurando não existe.", "notFoundButton": "Voltar para a tela inicial", @@ -37,21 +40,23 @@ "totpSuccessTitle": "Verificado", "totpSuccessSubtitle": "Redirecionando para o seu aplicativo", "totpTitle": "Insira o seu código TOTP", - "totpSubtitle": "Please enter the code from your authenticator app.", + "totpSubtitle": "Por favor, insira o código do seu aplicativo de autenticação.", "unauthorizedTitle": "Não autorizado", - "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", - "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", - "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", - "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", + "unauthorizedResourceSubtitle": "O usuário com nome de usuário {{username}} não está autorizado a acessar o recurso {{resource}}.", + "unauthorizedLoginSubtitle": "O usuário com o nome {{username}} não está autorizado a acessar.", + "unauthorizedGroupsSubtitle": "O usuário {{username}} não está autorizado a acessar o recurso {{resource}}.", + "unauthorizedIpSubtitle": "Seu endereço IP {{ip}} não está autorizado a acessar o recurso {{resource}}.", "unauthorizedButton": "Tentar novamente", - "untrustedRedirectTitle": "Redirecionamento não confiável", - "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", "cancelTitle": "Cancelar", "forgotPasswordTitle": "Esqueceu sua senha?", - "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", - "errorTitle": "An error occurred", - "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.", - "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", - "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "failedToFetchProvidersTitle": "Falha ao carregar provedores de autenticação. Verifique sua configuração.", + "errorTitle": "Ocorreu um erro", + "errorSubtitle": "Ocorreu um erro ao tentar executar esta ação. Por favor, verifique o console para mais informações.", + "forgotPasswordMessage": "Você pode redefinir sua senha alterando a variável de ambiente `USERS`.", + "fieldRequired": "Este campo é obrigatório", + "invalidInput": "Entrada Inválida", + "domainWarningTitle": "Domínio inválido", + "domainWarningSubtitle": "Esta instância está configurada para ser acessada de {{appUrl}}, mas {{currentUrl}} está sendo usado. Se você continuar, você pode encontrar problemas com a autenticação.", + "ignoreTitle": "Ignorar", + "goToCorrectDomainTitle": "Ir para o domínio correto" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/pt-PT.json b/frontend/src/lib/i18n/locales/pt-PT.json index 74e422f5..43004285 100644 --- a/frontend/src/lib/i18n/locales/pt-PT.json +++ b/frontend/src/lib/i18n/locales/pt-PT.json @@ -14,14 +14,17 @@ "loginOauthFailSubtitle": "Failed to get OAuth URL", "loginOauthSuccessTitle": "Redirecting", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", + "continueTitle": "Continue", "continueRedirectingTitle": "Redirecting...", "continueRedirectingSubtitle": "You should be redirected to the app soon", - "continueInvalidRedirectTitle": "Invalid redirect", - "continueInvalidRedirectSubtitle": "The redirect URL is invalid", + "continueRedirectManually": "Redirect me manually", "continueInsecureRedirectTitle": "Insecure redirect", "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", - "continueTitle": "Continue", - "continueSubtitle": "Click the button to continue to your app.", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", "logoutFailTitle": "Failed to log out", "logoutFailSubtitle": "Please try again", "logoutSuccessTitle": "Logged out", @@ -44,8 +47,6 @@ "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", "unauthorizedButton": "Try again", - "untrustedRedirectTitle": "Untrusted redirect", - "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", "cancelTitle": "Cancel", "forgotPasswordTitle": "Forgot your password?", "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", @@ -53,5 +54,9 @@ "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.", "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "invalidInput": "Invalid input", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/ro-RO.json b/frontend/src/lib/i18n/locales/ro-RO.json index 74e422f5..43004285 100644 --- a/frontend/src/lib/i18n/locales/ro-RO.json +++ b/frontend/src/lib/i18n/locales/ro-RO.json @@ -14,14 +14,17 @@ "loginOauthFailSubtitle": "Failed to get OAuth URL", "loginOauthSuccessTitle": "Redirecting", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", + "continueTitle": "Continue", "continueRedirectingTitle": "Redirecting...", "continueRedirectingSubtitle": "You should be redirected to the app soon", - "continueInvalidRedirectTitle": "Invalid redirect", - "continueInvalidRedirectSubtitle": "The redirect URL is invalid", + "continueRedirectManually": "Redirect me manually", "continueInsecureRedirectTitle": "Insecure redirect", "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", - "continueTitle": "Continue", - "continueSubtitle": "Click the button to continue to your app.", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", "logoutFailTitle": "Failed to log out", "logoutFailSubtitle": "Please try again", "logoutSuccessTitle": "Logged out", @@ -44,8 +47,6 @@ "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", "unauthorizedButton": "Try again", - "untrustedRedirectTitle": "Untrusted redirect", - "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", "cancelTitle": "Cancel", "forgotPasswordTitle": "Forgot your password?", "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", @@ -53,5 +54,9 @@ "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.", "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "invalidInput": "Invalid input", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/ru-RU.json b/frontend/src/lib/i18n/locales/ru-RU.json index f7b143fe..611c3d34 100644 --- a/frontend/src/lib/i18n/locales/ru-RU.json +++ b/frontend/src/lib/i18n/locales/ru-RU.json @@ -1,6 +1,6 @@ { "loginTitle": "С возвращением, войти с", - "loginTitleSimple": "Вход", + "loginTitleSimple": "С возвращением, пожалуйста войдите", "loginDivider": "Или", "loginUsername": "Имя пользователя", "loginPassword": "Пароль", @@ -8,24 +8,27 @@ "loginFailTitle": "Вход не удался", "loginFailSubtitle": "Проверьте имя пользователя и пароль", "loginFailRateLimit": "Слишком много ошибок входа. Попробуйте позже", - "loginSuccessTitle": "Вы вошли", + "loginSuccessTitle": "Вход выполнен", "loginSuccessSubtitle": "С возвращением!", "loginOauthFailTitle": "Произошла ошибка", - "loginOauthFailSubtitle": "Не удалось получить OAuth URL", + "loginOauthFailSubtitle": "Не удалось получить ссылку OAuth", "loginOauthSuccessTitle": "Перенаправление", "loginOauthSuccessSubtitle": "Перенаправление к поставщику OAuth", + "loginOauthAutoRedirectTitle": "OAuth автоматическое перенаправление", + "loginOauthAutoRedirectSubtitle": "Вы будете автоматически перенаправлены для авторизации у вашего поставщика OAuth.", + "loginOauthAutoRedirectButton": "Перенаправить сейчас", + "continueTitle": "Продолжить", "continueRedirectingTitle": "Перенаправление...", "continueRedirectingSubtitle": "Скоро вы будете перенаправлены в приложение", - "continueInvalidRedirectTitle": "Неверное перенаправление", - "continueInvalidRedirectSubtitle": "URL перенаправления недействителен", + "continueRedirectManually": "Перенаправить вручную", "continueInsecureRedirectTitle": "Небезопасное перенаправление", "continueInsecureRedirectSubtitle": "Попытка перенаправления с https на http, уверены, что хотите продолжить?", - "continueTitle": "Продолжить", - "continueSubtitle": "Нажмите на кнопку, чтобы перейти к приложению.", + "continueUntrustedRedirectTitle": "Недоверенное перенаправление", + "continueUntrustedRedirectSubtitle": "Вы пытаетесь перенаправить на домен, который не соответствует вашему настроенному домену ({{cookieDomain}}). Вы уверены, что хотите продолжить?", "logoutFailTitle": "Не удалось выйти", "logoutFailSubtitle": "Попробуйте ещё раз", "logoutSuccessTitle": "Выход", - "logoutSuccessSubtitle": "Вы вышли из системы", + "logoutSuccessSubtitle": "Вы вышли", "logoutTitle": "Выйти", "logoutUsernameSubtitle": "Вход выполнен как {{username}}, нажмите на кнопку ниже, чтобы выйти.", "logoutOauthSubtitle": "Вход выполнен как {{username}} с использованием {{provider}} OAuth, нажмите кнопку ниже, чтобы выйти.", @@ -37,21 +40,23 @@ "totpSuccessTitle": "Подтверждён", "totpSuccessSubtitle": "Перенаправление в приложение", "totpTitle": "Введите код TOTP", - "totpSubtitle": "Пожалуйста, введите код из вашего приложения — аутентификатора.", - "unauthorizedTitle": "Доступ запрещен", - "unauthorizedResourceSubtitle": "Пользователю {{username}} не разрешен доступ к {{resource}}.", - "unauthorizedLoginSubtitle": "Пользователю {{username}} не разрешен вход.", - "unauthorizedGroupsSubtitle": "Пользователь {{username}} не состоит в группах, которым разрешен доступ к {{resource}}.", - "unauthorizedIpSubtitle": "Ваш IP адрес {{ip}} не авторизован для доступа к ресурсу {{resource}}.", + "totpSubtitle": "Пожалуйста, введите код из вашего приложения авторизации.", + "unauthorizedTitle": "Доступ запрещён", + "unauthorizedResourceSubtitle": "Пользователю {{username}} не разрешён доступ к {{resource}}.", + "unauthorizedLoginSubtitle": "Пользователю {{username}} не разрешён вход.", + "unauthorizedGroupsSubtitle": "Пользователь {{username}} не состоит в группах, которым разрешён доступ к {{resource}}.", + "unauthorizedIpSubtitle": "Вашему IP-адресу {{ip}} не разрешён доступ к ресурсу {{resource}}.", "unauthorizedButton": "Повторить", - "untrustedRedirectTitle": "Ненадежное перенаправление", - "untrustedRedirectSubtitle": "Попытка перенаправить на домен, который не соответствует вашему заданному домену ({{domain}}). Уверены, что хотите продолжить?", "cancelTitle": "Отмена", "forgotPasswordTitle": "Забыли пароль?", - "failedToFetchProvidersTitle": "Не удалось загрузить провайдеров аутентификации. Пожалуйста, проверьте конфигурацию.", + "failedToFetchProvidersTitle": "Не удалось загрузить поставщика авторизации. Пожалуйста, проверьте конфигурацию.", "errorTitle": "Произошла ошибка", "errorSubtitle": "Произошла ошибка при попытке выполнить это действие. Проверьте консоль для дополнительной информации.", - "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", - "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "forgotPasswordMessage": "Вы можете сбросить свой пароль, изменив переменную окружения `USERS`.", + "fieldRequired": "Это поле является обязательным", + "invalidInput": "Недопустимый ввод", + "domainWarningTitle": "Неверный домен", + "domainWarningSubtitle": "Этот экземпляр настроен на доступ к нему из {{appUrl}}, но {{currentUrl}} в настоящее время используется. Если вы продолжите, то могут возникнуть проблемы с авторизацией.", + "ignoreTitle": "Игнорировать", + "goToCorrectDomainTitle": "Перейти к правильному домену" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/sr-SP.json b/frontend/src/lib/i18n/locales/sr-SP.json index 34df08d7..f7460e3b 100644 --- a/frontend/src/lib/i18n/locales/sr-SP.json +++ b/frontend/src/lib/i18n/locales/sr-SP.json @@ -14,14 +14,17 @@ "loginOauthFailSubtitle": "Неуспело преузимање OAuth адресе", "loginOauthSuccessTitle": "Преусмеравање", "loginOauthSuccessSubtitle": "Преусмеравање на вашег OAuth провајдера", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", + "continueTitle": "Настави", "continueRedirectingTitle": "Преусмеравање...", "continueRedirectingSubtitle": "Требали би сте ускоро да будете преусмерени на апликацију", - "continueInvalidRedirectTitle": "Неисправно преусмеравање", - "continueInvalidRedirectSubtitle": "Адреса за преусмеравање није исправна", + "continueRedirectManually": "Redirect me manually", "continueInsecureRedirectTitle": "Небезбедно преусмеравање", "continueInsecureRedirectSubtitle": "Покушавате да преусмерите са https на http што није безбедно. Да ли желите да наставите?", - "continueTitle": "Настави", - "continueSubtitle": "Кликните на дугме да би сте наставили на нашу апликацију.", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", "logoutFailTitle": "Неуспешно одјављивање", "logoutFailSubtitle": "Молим вас покушајте поново", "logoutSuccessTitle": "Одјављени", @@ -44,8 +47,6 @@ "unauthorizedGroupsSubtitle": "Корисник са корисничким именом {{username}} није у групама које захтева ресурс {{resource}}.", "unauthorizedIpSubtitle": "Ваша IP адреса {{ip}} није ауторизована да приступи ресурсу {{resource}}.", "unauthorizedButton": "Покушајте поново", - "untrustedRedirectTitle": "Преусмерење без поверења", - "untrustedRedirectSubtitle": "Покушавате да преусмерите на домен који се не поклапа са подешеним доменом ({{domain}}). Да ли желите да наставите?", "cancelTitle": "Поништи", "forgotPasswordTitle": "Заборавили сте лозинку?", "failedToFetchProvidersTitle": "Није успело учитавање провајдера аутентификације. Молим вас проверите ваша подешавања.", @@ -53,5 +54,9 @@ "errorSubtitle": "Појавила се грешка при покушају извршавања ове радње. Молим вас проверите конзолу за додатне информације.", "forgotPasswordMessage": "Можете поништити вашу лозинку променом `USERS` променљиве окружења.", "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "invalidInput": "Invalid input", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/sv-SE.json b/frontend/src/lib/i18n/locales/sv-SE.json index 74e422f5..be1a6288 100644 --- a/frontend/src/lib/i18n/locales/sv-SE.json +++ b/frontend/src/lib/i18n/locales/sv-SE.json @@ -1,32 +1,35 @@ { - "loginTitle": "Welcome back, login with", - "loginTitleSimple": "Welcome back, please login", - "loginDivider": "Or", - "loginUsername": "Username", - "loginPassword": "Password", - "loginSubmit": "Login", - "loginFailTitle": "Failed to log in", - "loginFailSubtitle": "Please check your username and password", - "loginFailRateLimit": "You failed to login too many times. Please try again later", - "loginSuccessTitle": "Logged in", - "loginSuccessSubtitle": "Welcome back!", - "loginOauthFailTitle": "An error occurred", - "loginOauthFailSubtitle": "Failed to get OAuth URL", - "loginOauthSuccessTitle": "Redirecting", - "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", - "continueRedirectingTitle": "Redirecting...", - "continueRedirectingSubtitle": "You should be redirected to the app soon", - "continueInvalidRedirectTitle": "Invalid redirect", - "continueInvalidRedirectSubtitle": "The redirect URL is invalid", - "continueInsecureRedirectTitle": "Insecure redirect", - "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", - "continueTitle": "Continue", - "continueSubtitle": "Click the button to continue to your app.", - "logoutFailTitle": "Failed to log out", - "logoutFailSubtitle": "Please try again", - "logoutSuccessTitle": "Logged out", - "logoutSuccessSubtitle": "You have been logged out", - "logoutTitle": "Logout", + "loginTitle": "Välkommen tillbaka, logga in med", + "loginTitleSimple": "Välkommen tillbaka, logga in", + "loginDivider": "Eller", + "loginUsername": "Användarnamn", + "loginPassword": "Lösenord", + "loginSubmit": "Logga in", + "loginFailTitle": "Kunde inte logga in", + "loginFailSubtitle": "Kontrollera ditt användarnamn och lösenord", + "loginFailRateLimit": "Du misslyckades med att logga in för många gånger. Försök igen senare", + "loginSuccessTitle": "Inloggad", + "loginSuccessSubtitle": "Välkommen tillbaka!", + "loginOauthFailTitle": "Ett fel har uppstått", + "loginOauthFailSubtitle": "Kunde inte hämta OAuth URL", + "loginOauthSuccessTitle": "Omdirigerar", + "loginOauthSuccessSubtitle": "Omdirigera till din OAuth leverantör", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", + "continueTitle": "Fortsätt", + "continueRedirectingTitle": "Omdirigerar...", + "continueRedirectingSubtitle": "Du bör omdirigeras till appen snart", + "continueRedirectManually": "Redirect me manually", + "continueInsecureRedirectTitle": "Osäker omdirigering", + "continueInsecureRedirectSubtitle": "Du försöker omdirigera från https till http som inte är säker. Är du säker på att du vill fortsätta?", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", + "logoutFailTitle": "Kunde inte logga ut.", + "logoutFailSubtitle": "Vänligen försök igen", + "logoutSuccessTitle": "Utloggad", + "logoutSuccessSubtitle": "Du har blivit utloggad", + "logoutTitle": "Logga ut", "logoutUsernameSubtitle": "You are currently logged in as {{username}}. Click the button below to logout.", "logoutOauthSubtitle": "You are currently logged in as {{username}} using the {{provider}} OAuth provider. Click the button below to logout.", "notFoundTitle": "Page not found", @@ -38,14 +41,12 @@ "totpSuccessSubtitle": "Redirecting to your app", "totpTitle": "Enter your TOTP code", "totpSubtitle": "Please enter the code from your authenticator app.", - "unauthorizedTitle": "Unauthorized", + "unauthorizedTitle": "Obehörig", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", "unauthorizedButton": "Try again", - "untrustedRedirectTitle": "Untrusted redirect", - "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", "cancelTitle": "Cancel", "forgotPasswordTitle": "Forgot your password?", "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", @@ -53,5 +54,9 @@ "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.", "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "invalidInput": "Invalid input", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/tr-TR.json b/frontend/src/lib/i18n/locales/tr-TR.json index df6c3c33..af2fdc02 100644 --- a/frontend/src/lib/i18n/locales/tr-TR.json +++ b/frontend/src/lib/i18n/locales/tr-TR.json @@ -14,14 +14,17 @@ "loginOauthFailSubtitle": "Failed to get OAuth URL", "loginOauthSuccessTitle": "Yönlendiriliyor", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", + "continueTitle": "Devam et", "continueRedirectingTitle": "Yönlendiriliyor...", "continueRedirectingSubtitle": "You should be redirected to the app soon", - "continueInvalidRedirectTitle": "Invalid redirect", - "continueInvalidRedirectSubtitle": "The redirect URL is invalid", + "continueRedirectManually": "Redirect me manually", "continueInsecureRedirectTitle": "Insecure redirect", "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", - "continueTitle": "Devam et", - "continueSubtitle": "Click the button to continue to your app.", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", "logoutFailTitle": "Failed to log out", "logoutFailSubtitle": "Lütfen tekrar deneyin", "logoutSuccessTitle": "Çıkış yapıldı", @@ -44,8 +47,6 @@ "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", "unauthorizedButton": "Try again", - "untrustedRedirectTitle": "Untrusted redirect", - "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", "cancelTitle": "İptal", "forgotPasswordTitle": "Forgot your password?", "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", @@ -53,5 +54,9 @@ "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.", "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "invalidInput": "Invalid input", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/uk-UA.json b/frontend/src/lib/i18n/locales/uk-UA.json index 74e422f5..60f8ad38 100644 --- a/frontend/src/lib/i18n/locales/uk-UA.json +++ b/frontend/src/lib/i18n/locales/uk-UA.json @@ -1,5 +1,5 @@ { - "loginTitle": "Welcome back, login with", + "loginTitle": "З поверненням, увійдіть через", "loginTitleSimple": "Welcome back, please login", "loginDivider": "Or", "loginUsername": "Username", @@ -14,14 +14,17 @@ "loginOauthFailSubtitle": "Failed to get OAuth URL", "loginOauthSuccessTitle": "Redirecting", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", + "continueTitle": "Continue", "continueRedirectingTitle": "Redirecting...", "continueRedirectingSubtitle": "You should be redirected to the app soon", - "continueInvalidRedirectTitle": "Invalid redirect", - "continueInvalidRedirectSubtitle": "The redirect URL is invalid", + "continueRedirectManually": "Redirect me manually", "continueInsecureRedirectTitle": "Insecure redirect", "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", - "continueTitle": "Continue", - "continueSubtitle": "Click the button to continue to your app.", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", "logoutFailTitle": "Failed to log out", "logoutFailSubtitle": "Please try again", "logoutSuccessTitle": "Logged out", @@ -44,8 +47,6 @@ "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", "unauthorizedButton": "Try again", - "untrustedRedirectTitle": "Untrusted redirect", - "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", "cancelTitle": "Cancel", "forgotPasswordTitle": "Forgot your password?", "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", @@ -53,5 +54,9 @@ "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.", "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "invalidInput": "Invalid input", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/vi-VN.json b/frontend/src/lib/i18n/locales/vi-VN.json index 74e422f5..43004285 100644 --- a/frontend/src/lib/i18n/locales/vi-VN.json +++ b/frontend/src/lib/i18n/locales/vi-VN.json @@ -14,14 +14,17 @@ "loginOauthFailSubtitle": "Failed to get OAuth URL", "loginOauthSuccessTitle": "Redirecting", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", + "continueTitle": "Continue", "continueRedirectingTitle": "Redirecting...", "continueRedirectingSubtitle": "You should be redirected to the app soon", - "continueInvalidRedirectTitle": "Invalid redirect", - "continueInvalidRedirectSubtitle": "The redirect URL is invalid", + "continueRedirectManually": "Redirect me manually", "continueInsecureRedirectTitle": "Insecure redirect", "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", - "continueTitle": "Continue", - "continueSubtitle": "Click the button to continue to your app.", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", "logoutFailTitle": "Failed to log out", "logoutFailSubtitle": "Please try again", "logoutSuccessTitle": "Logged out", @@ -44,8 +47,6 @@ "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", "unauthorizedButton": "Try again", - "untrustedRedirectTitle": "Untrusted redirect", - "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", "cancelTitle": "Cancel", "forgotPasswordTitle": "Forgot your password?", "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", @@ -53,5 +54,9 @@ "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.", "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "invalidInput": "Invalid input", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/zh-CN.json b/frontend/src/lib/i18n/locales/zh-CN.json index e29a00f9..25b50ff2 100644 --- a/frontend/src/lib/i18n/locales/zh-CN.json +++ b/frontend/src/lib/i18n/locales/zh-CN.json @@ -14,14 +14,17 @@ "loginOauthFailSubtitle": "获取 OAuth URL 失败", "loginOauthSuccessTitle": "重定向中", "loginOauthSuccessSubtitle": "重定向到您的 OAuth 提供商", + "loginOauthAutoRedirectTitle": "OAuth自动重定向", + "loginOauthAutoRedirectSubtitle": "您将被自动重定向到您的 OAuth 提供商进行身份验证。", + "loginOauthAutoRedirectButton": "立即跳转", + "continueTitle": "继续", "continueRedirectingTitle": "正在重定向……", "continueRedirectingSubtitle": "您应该很快被重定向到应用", - "continueInvalidRedirectTitle": "无效的重定向", - "continueInvalidRedirectSubtitle": "重定向URL无效", + "continueRedirectManually": "请手动跳转", "continueInsecureRedirectTitle": "不安全的重定向", "continueInsecureRedirectSubtitle": "您正在尝试从https重定向到http可能存在风险。您确定要继续吗?", - "continueTitle": "继续", - "continueSubtitle": "点击按钮以继续您的应用。", + "continueUntrustedRedirectTitle": "不可信的重定向", + "continueUntrustedRedirectSubtitle": "您尝试跳转的域名与配置的域名({{cookieDomain}})不匹配。是否继续?", "logoutFailTitle": "注销失败", "logoutFailSubtitle": "请重试", "logoutSuccessTitle": "已登出", @@ -30,7 +33,7 @@ "logoutUsernameSubtitle": "您当前登录用户为{{username}}。点击下方按钮注销。", "logoutOauthSubtitle": "您当前以{{username}}登录,使用的是{{provider}} OAuth 提供商。点击下方按钮注销。", "notFoundTitle": "无法找到页面", - "notFoundSubtitle": "您正在查找的页面不存在。", + "notFoundSubtitle": "您访问的页面不存在。", "notFoundButton": "回到主页", "totpFailTitle": "无法验证代码", "totpFailSubtitle": "请检查您的代码并重试", @@ -42,16 +45,18 @@ "unauthorizedResourceSubtitle": "用户名为{{username}}的用户无权访问资源{{resource}}。", "unauthorizedLoginSubtitle": "用户名为{{username}}的用户无权登录。", "unauthorizedGroupsSubtitle": "用户名为{{username}}的用户不在资源{{resource}}所需的组中。", - "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", + "unauthorizedIpSubtitle": "用户 {{ip}} 无权访问资源 {{resource}}。", "unauthorizedButton": "重试", - "untrustedRedirectTitle": "不可信的重定向", - "untrustedRedirectSubtitle": "您正在尝试重定向到一个与您已配置的域名 ({{domain}}) 不匹配的域名。您确定要继续吗?", "cancelTitle": "取消", "forgotPasswordTitle": "忘记密码?", "failedToFetchProvidersTitle": "加载身份验证提供程序失败,请检查您的配置。", "errorTitle": "发生了错误", "errorSubtitle": "执行此操作时发生错误,请检查控制台以获取更多信息。", - "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", - "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "forgotPasswordMessage": "您可以通过更改 `USERS ` 环境变量重置您的密码。", + "fieldRequired": "必添字段", + "invalidInput": "无效的输入", + "domainWarningTitle": "无效域名", + "domainWarningSubtitle": "当前实例配置的访问地址为 {{appUrl}},但您正在使用 {{currentUrl}}。若继续操作,可能会遇到身份验证问题。", + "ignoreTitle": "忽略", + "goToCorrectDomainTitle": "转到正确的域名" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/zh-TW.json b/frontend/src/lib/i18n/locales/zh-TW.json index 8c324303..d7b2cb1e 100644 --- a/frontend/src/lib/i18n/locales/zh-TW.json +++ b/frontend/src/lib/i18n/locales/zh-TW.json @@ -14,14 +14,17 @@ "loginOauthFailSubtitle": "無法取得 OAuth 網址", "loginOauthSuccessTitle": "重新導向中", "loginOauthSuccessSubtitle": "正在將您重新導向至 OAuth 供應商", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", + "continueTitle": "繼續", "continueRedirectingTitle": "重新導向中……", "continueRedirectingSubtitle": "您即將被重新導向至應用程式", - "continueInvalidRedirectTitle": "無效的重新導向", - "continueInvalidRedirectSubtitle": "重新導向的網址無效", + "continueRedirectManually": "Redirect me manually", "continueInsecureRedirectTitle": "不安全的重新導向", "continueInsecureRedirectSubtitle": "您正嘗試從安全的 https 重新導向至不安全的 http。您確定要繼續嗎?", - "continueTitle": "繼續", - "continueSubtitle": "點擊按鈕以繼續前往您的應用程式。", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", "logoutFailTitle": "登出失敗", "logoutFailSubtitle": "請再試一次", "logoutSuccessTitle": "登出成功", @@ -44,8 +47,6 @@ "unauthorizedGroupsSubtitle": "使用者 {{username}} 不在存取資源 {{resource}} 所需的群組中。", "unauthorizedIpSubtitle": "您的 IP 位址 {{ip}} 未被授權存取資源 {{resource}}。", "unauthorizedButton": "再試一次", - "untrustedRedirectTitle": "不受信任的重新導向", - "untrustedRedirectSubtitle": "您正嘗試重新導向至的網域與您設定的網域 ({{domain}}) 不符。您確定要繼續嗎?", "cancelTitle": "取消", "forgotPasswordTitle": "忘記密碼?", "failedToFetchProvidersTitle": "載入驗證供應商失敗。請檢查您的設定。", @@ -53,5 +54,9 @@ "errorSubtitle": "執行此操作時發生錯誤。請檢查主控台以獲取更多資訊。", "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", "fieldRequired": "This field is required", - "invalidInput": "Invalid input" + "invalidInput": "Invalid input", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain" } \ No newline at end of file From 2f8fa39a9bfeb487e352ff83ecb5391b8a127326 Mon Sep 17 00:00:00 2001 From: Stavros Date: Mon, 6 Oct 2025 21:27:51 +0300 Subject: [PATCH 049/127] refactor: make cli modular (#390) * refactor: make cli modular * chore: apply suggestion from @Rycochet Co-authored-by: Ryc O'Chet * chore: apply review suggestions * refactor: no need to handle user escaping in verify cmd --------- Co-authored-by: Ryc O'Chet --- cmd/create.go | 99 +++++++++++++++++++++++++ cmd/generate.go | 120 ++++++++++++++++++++++++++++++ cmd/root.go | 133 +++++++++++++++++++++------------- cmd/totp/generate/generate.go | 99 ------------------------- cmd/totp/totp.go | 17 ----- cmd/user/create/create.go | 80 -------------------- cmd/user/user.go | 19 ----- cmd/user/verify/verify.go | 101 -------------------------- cmd/verify.go | 118 ++++++++++++++++++++++++++++++ cmd/version.go | 41 ++++++++--- main.go | 2 +- 11 files changed, 452 insertions(+), 377 deletions(-) create mode 100644 cmd/create.go create mode 100644 cmd/generate.go delete mode 100644 cmd/totp/generate/generate.go delete mode 100644 cmd/totp/totp.go delete mode 100644 cmd/user/create/create.go delete mode 100644 cmd/user/user.go delete mode 100644 cmd/user/verify/verify.go create mode 100644 cmd/verify.go diff --git a/cmd/create.go b/cmd/create.go new file mode 100644 index 00000000..0abb3c7d --- /dev/null +++ b/cmd/create.go @@ -0,0 +1,99 @@ +package cmd + +import ( + "errors" + "fmt" + "strings" + + "github.com/charmbracelet/huh" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + "golang.org/x/crypto/bcrypt" +) + +type createUserCmd struct { + root *cobra.Command + cmd *cobra.Command + + interactive bool + docker bool + username string + password string +} + +func newCreateUserCmd(root *cobra.Command) *createUserCmd { + return &createUserCmd{ + root: root, + } +} + +func (c *createUserCmd) Register() { + c.cmd = &cobra.Command{ + Use: "create", + Short: "Create a user", + Long: `Create a user either interactively or by passing flags.`, + Run: c.run, + } + + c.cmd.Flags().BoolVarP(&c.interactive, "interactive", "i", false, "Create a user interactively") + c.cmd.Flags().BoolVar(&c.docker, "docker", false, "Format output for docker") + c.cmd.Flags().StringVar(&c.username, "username", "", "Username") + c.cmd.Flags().StringVar(&c.password, "password", "", "Password") + + if c.root != nil { + c.root.AddCommand(c.cmd) + } +} + +func (c *createUserCmd) GetCmd() *cobra.Command { + return c.cmd +} + +func (c *createUserCmd) run(cmd *cobra.Command, args []string) { + log.Logger = log.Level(zerolog.InfoLevel) + + if c.interactive { + form := huh.NewForm( + huh.NewGroup( + huh.NewInput().Title("Username").Value(&c.username).Validate((func(s string) error { + if s == "" { + return errors.New("username cannot be empty") + } + return nil + })), + huh.NewInput().Title("Password").Value(&c.password).Validate((func(s string) error { + if s == "" { + return errors.New("password cannot be empty") + } + return nil + })), + huh.NewSelect[bool]().Title("Format the output for Docker?").Options(huh.NewOption("Yes", true), huh.NewOption("No", false)).Value(&c.docker), + ), + ) + var baseTheme *huh.Theme = huh.ThemeBase() + err := form.WithTheme(baseTheme).Run() + if err != nil { + log.Fatal().Err(err).Msg("Form failed") + } + } + + if c.username == "" || c.password == "" { + log.Fatal().Err(errors.New("error invalid input")).Msg("Username and password cannot be empty") + } + + log.Info().Str("username", c.username).Msg("Creating user") + + passwd, err := bcrypt.GenerateFromPassword([]byte(c.password), bcrypt.DefaultCost) + if err != nil { + log.Fatal().Err(err).Msg("Failed to hash password") + } + + // If docker format is enabled, escape the dollar sign + passwdStr := string(passwd) + if c.docker { + passwdStr = strings.ReplaceAll(passwdStr, "$", "$$") + } + + log.Info().Str("user", fmt.Sprintf("%s:%s", c.username, passwdStr)).Msg("User created") +} diff --git a/cmd/generate.go b/cmd/generate.go new file mode 100644 index 00000000..005b4738 --- /dev/null +++ b/cmd/generate.go @@ -0,0 +1,120 @@ +package cmd + +import ( + "errors" + "fmt" + "os" + "strings" + "tinyauth/internal/utils" + + "github.com/charmbracelet/huh" + "github.com/mdp/qrterminal/v3" + "github.com/pquerna/otp/totp" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +type generateTotpCmd struct { + root *cobra.Command + cmd *cobra.Command + + interactive bool + user string +} + +func newGenerateTotpCmd(root *cobra.Command) *generateTotpCmd { + return &generateTotpCmd{ + root: root, + } +} + +func (c *generateTotpCmd) Register() { + c.cmd = &cobra.Command{ + Use: "generate", + Short: "Generate a totp secret", + Long: `Generate a totp secret for a user either interactively or by passing flags.`, + Run: c.run, + } + + c.cmd.Flags().BoolVarP(&c.interactive, "interactive", "i", false, "Run in interactive mode") + c.cmd.Flags().StringVar(&c.user, "user", "", "Your current user (username:hash)") + + if c.root != nil { + c.root.AddCommand(c.cmd) + } +} + +func (c *generateTotpCmd) GetCmd() *cobra.Command { + return c.cmd +} + +func (c *generateTotpCmd) run(cmd *cobra.Command, args []string) { + log.Logger = log.Level(zerolog.InfoLevel) + + if c.interactive { + form := huh.NewForm( + huh.NewGroup( + huh.NewInput().Title("Current user (username:hash)").Value(&c.user).Validate((func(s string) error { + if s == "" { + return errors.New("user cannot be empty") + } + return nil + })), + ), + ) + var baseTheme *huh.Theme = huh.ThemeBase() + err := form.WithTheme(baseTheme).Run() + if err != nil { + log.Fatal().Err(err).Msg("Form failed") + } + } + + user, err := utils.ParseUser(c.user) + if err != nil { + log.Fatal().Err(err).Msg("Failed to parse user") + } + + docker := false + if strings.Contains(c.user, "$$") { + docker = true + } + + if user.TotpSecret != "" { + log.Fatal().Msg("User already has a TOTP secret") + } + + key, err := totp.Generate(totp.GenerateOpts{ + Issuer: "Tinyauth", + AccountName: user.Username, + }) + + if err != nil { + log.Fatal().Err(err).Msg("Failed to generate TOTP secret") + } + + secret := key.Secret() + + log.Info().Str("secret", secret).Msg("Generated TOTP secret") + + log.Info().Msg("Generated QR code") + + config := qrterminal.Config{ + Level: qrterminal.L, + Writer: os.Stdout, + BlackChar: qrterminal.BLACK, + WhiteChar: qrterminal.WHITE, + QuietZone: 2, + } + + qrterminal.GenerateWithConfig(key.URL(), config) + + user.TotpSecret = secret + + // If using docker escape re-escape it + if docker { + user.Password = strings.ReplaceAll(user.Password, "$", "$$") + } + + log.Info().Str("user", fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TotpSecret)).Msg("Add the totp secret to your authenticator app then use the verify command to ensure everything is working correctly.") +} diff --git a/cmd/root.go b/cmd/root.go index 723cb368..e7bbb133 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,8 +2,6 @@ package cmd import ( "strings" - totpCmd "tinyauth/cmd/totp" - userCmd "tinyauth/cmd/user" "tinyauth/internal/bootstrap" "tinyauth/internal/config" "tinyauth/internal/utils" @@ -15,55 +13,28 @@ import ( "github.com/spf13/viper" ) -var rootCmd = &cobra.Command{ - Use: "tinyauth", - Short: "The simplest way to protect your apps with a login screen.", - Long: `Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic OAuth provider to all of your docker apps.`, - Run: func(cmd *cobra.Command, args []string) { - var conf config.Config +type rootCmd struct { + root *cobra.Command + cmd *cobra.Command - err := viper.Unmarshal(&conf) - if err != nil { - log.Fatal().Err(err).Msg("Failed to parse config") - } - - // Validate config - v := validator.New() - - err = v.Struct(conf) - if err != nil { - log.Fatal().Err(err).Msg("Invalid config") - } - - log.Logger = log.Level(zerolog.Level(utils.GetLogLevel(conf.LogLevel))) - log.Info().Str("version", strings.TrimSpace(config.Version)).Msg("Starting tinyauth") - - // Create bootstrap app - app := bootstrap.NewBootstrapApp(conf) - - // Run - err = app.Setup() - - if err != nil { - log.Fatal().Err(err).Msg("Failed to setup app") - } - - }, + viper *viper.Viper } -func Execute() { - rootCmd.FParseErrWhitelist.UnknownFlags = true - err := rootCmd.Execute() - if err != nil { - log.Fatal().Err(err).Msg("Failed to execute command") +func newRootCmd() *rootCmd { + return &rootCmd{ + viper: viper.New(), } } -func init() { - rootCmd.AddCommand(userCmd.UserCmd()) - rootCmd.AddCommand(totpCmd.TotpCmd()) +func (c *rootCmd) Register() { + c.cmd = &cobra.Command{ + Use: "tinyauth", + Short: "The simplest way to protect your apps with a login screen", + Long: `Tinyauth is a simple authentication middleware that adds a simple login screen or OAuth with Google, Github or any other provider to all of your docker apps.`, + Run: c.run, + } - viper.AutomaticEnv() + c.viper.AutomaticEnv() configOptions := []struct { name string @@ -101,17 +72,81 @@ func init() { for _, opt := range configOptions { switch v := opt.defaultVal.(type) { case bool: - rootCmd.Flags().Bool(opt.name, v, opt.description) + c.cmd.Flags().Bool(opt.name, v, opt.description) case int: - rootCmd.Flags().Int(opt.name, v, opt.description) + c.cmd.Flags().Int(opt.name, v, opt.description) case string: - rootCmd.Flags().String(opt.name, v, opt.description) + c.cmd.Flags().String(opt.name, v, opt.description) } // Create uppercase env var name envVar := strings.ReplaceAll(strings.ToUpper(opt.name), "-", "_") - viper.BindEnv(opt.name, envVar) + c.viper.BindEnv(opt.name, envVar) + } + + c.viper.BindPFlags(c.cmd.Flags()) + + if c.root != nil { + c.root.AddCommand(c.cmd) + } +} + +func (c *rootCmd) GetCmd() *cobra.Command { + return c.cmd +} + +func (c *rootCmd) run(cmd *cobra.Command, args []string) { + var conf config.Config + + err := c.viper.Unmarshal(&conf) + if err != nil { + log.Fatal().Err(err).Msg("Failed to parse config") + } + + v := validator.New() + err = v.Struct(conf) + if err != nil { + log.Fatal().Err(err).Msg("Invalid config") + } + + log.Logger = log.Level(zerolog.Level(utils.GetLogLevel(conf.LogLevel))) + log.Info().Str("version", strings.TrimSpace(config.Version)).Msg("Starting Tinyauth") + + app := bootstrap.NewBootstrapApp(conf) + + err = app.Setup() + if err != nil { + log.Fatal().Err(err).Msg("Failed to setup app") + } +} + +func Run() { + rootCmd := newRootCmd() + rootCmd.Register() + root := rootCmd.GetCmd() + + userCmd := &cobra.Command{ + Use: "user", + Short: "User utilities", + Long: `Utilities for creating and verifying tinyauth compatible users.`, + } + totpCmd := &cobra.Command{ + Use: "totp", + Short: "Totp utilities", + Long: `Utilities for creating and verifying totp codes.`, } - viper.BindPFlags(rootCmd.Flags()) + newCreateUserCmd(userCmd).Register() + newVerifyUserCmd(userCmd).Register() + newGenerateTotpCmd(totpCmd).Register() + newVersionCmd(root).Register() + + root.AddCommand(userCmd) + root.AddCommand(totpCmd) + + err := root.Execute() + + if err != nil { + log.Fatal().Err(err).Msg("Failed to execute root command") + } } diff --git a/cmd/totp/generate/generate.go b/cmd/totp/generate/generate.go deleted file mode 100644 index 72f0c297..00000000 --- a/cmd/totp/generate/generate.go +++ /dev/null @@ -1,99 +0,0 @@ -package generate - -import ( - "errors" - "fmt" - "os" - "strings" - "tinyauth/internal/utils" - - "github.com/charmbracelet/huh" - "github.com/mdp/qrterminal/v3" - "github.com/pquerna/otp/totp" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" -) - -var interactive bool - -// Input user -var iUser string - -var GenerateCmd = &cobra.Command{ - Use: "generate", - Short: "Generate a totp secret", - Run: func(cmd *cobra.Command, args []string) { - log.Logger = log.Level(zerolog.InfoLevel) - - if interactive { - form := huh.NewForm( - huh.NewGroup( - huh.NewInput().Title("Current username:hash").Value(&iUser).Validate((func(s string) error { - if s == "" { - return errors.New("user cannot be empty") - } - return nil - })), - ), - ) - var baseTheme *huh.Theme = huh.ThemeBase() - err := form.WithTheme(baseTheme).Run() - if err != nil { - log.Fatal().Err(err).Msg("Form failed") - } - } - - user, err := utils.ParseUser(iUser) - if err != nil { - log.Fatal().Err(err).Msg("Failed to parse user") - } - - dockerEscape := false - if strings.Contains(iUser, "$$") { - dockerEscape = true - } - - if user.TotpSecret != "" { - log.Fatal().Msg("User already has a totp secret") - } - - key, err := totp.Generate(totp.GenerateOpts{ - Issuer: "Tinyauth", - AccountName: user.Username, - }) - if err != nil { - log.Fatal().Err(err).Msg("Failed to generate totp secret") - } - - secret := key.Secret() - - log.Info().Str("secret", secret).Msg("Generated totp secret") - - log.Info().Msg("Generated QR code") - - config := qrterminal.Config{ - Level: qrterminal.L, - Writer: os.Stdout, - BlackChar: qrterminal.BLACK, - WhiteChar: qrterminal.WHITE, - QuietZone: 2, - } - - qrterminal.GenerateWithConfig(key.URL(), config) - - user.TotpSecret = secret - - // If using docker escape re-escape it - if dockerEscape { - user.Password = strings.ReplaceAll(user.Password, "$", "$$") - } - - log.Info().Str("user", fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TotpSecret)).Msg("Add the totp secret to your authenticator app then use the verify command to ensure everything is working correctly.") - }, -} - -func init() { - GenerateCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Run in interactive mode") - GenerateCmd.Flags().StringVar(&iUser, "user", "", "Your current username:hash") -} diff --git a/cmd/totp/totp.go b/cmd/totp/totp.go deleted file mode 100644 index bfe08aa9..00000000 --- a/cmd/totp/totp.go +++ /dev/null @@ -1,17 +0,0 @@ -package cmd - -import ( - "tinyauth/cmd/totp/generate" - - "github.com/spf13/cobra" -) - -func TotpCmd() *cobra.Command { - totpCmd := &cobra.Command{ - Use: "totp", - Short: "Totp utilities", - Long: `Utilities for creating and verifying totp codes.`, - } - totpCmd.AddCommand(generate.GenerateCmd) - return totpCmd -} diff --git a/cmd/user/create/create.go b/cmd/user/create/create.go deleted file mode 100644 index ca5f95e0..00000000 --- a/cmd/user/create/create.go +++ /dev/null @@ -1,80 +0,0 @@ -package create - -import ( - "errors" - "fmt" - "strings" - - "github.com/charmbracelet/huh" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - "golang.org/x/crypto/bcrypt" -) - -var interactive bool -var docker bool - -// i stands for input -var iUsername string -var iPassword string - -var CreateCmd = &cobra.Command{ - Use: "create", - Short: "Create a user", - Long: `Create a user either interactively or by passing flags.`, - Run: func(cmd *cobra.Command, args []string) { - log.Logger = log.Level(zerolog.InfoLevel) - - if interactive { - form := huh.NewForm( - huh.NewGroup( - huh.NewInput().Title("Username").Value(&iUsername).Validate((func(s string) error { - if s == "" { - return errors.New("username cannot be empty") - } - return nil - })), - huh.NewInput().Title("Password").Value(&iPassword).Validate((func(s string) error { - if s == "" { - return errors.New("password cannot be empty") - } - return nil - })), - huh.NewSelect[bool]().Title("Format the output for docker?").Options(huh.NewOption("Yes", true), huh.NewOption("No", false)).Value(&docker), - ), - ) - var baseTheme *huh.Theme = huh.ThemeBase() - err := form.WithTheme(baseTheme).Run() - if err != nil { - log.Fatal().Err(err).Msg("Form failed") - } - } - - if iUsername == "" || iPassword == "" { - log.Fatal().Err(errors.New("error invalid input")).Msg("Username and password cannot be empty") - } - - log.Info().Str("username", iUsername).Str("password", iPassword).Bool("docker", docker).Msg("Creating user") - - password, err := bcrypt.GenerateFromPassword([]byte(iPassword), bcrypt.DefaultCost) - if err != nil { - log.Fatal().Err(err).Msg("Failed to hash password") - } - - // If docker format is enabled, escape the dollar sign - passwordString := string(password) - if docker { - passwordString = strings.ReplaceAll(passwordString, "$", "$$") - } - - log.Info().Str("user", fmt.Sprintf("%s:%s", iUsername, passwordString)).Msg("User created") - }, -} - -func init() { - CreateCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Create a user interactively") - CreateCmd.Flags().BoolVar(&docker, "docker", false, "Format output for docker") - CreateCmd.Flags().StringVar(&iUsername, "username", "", "Username") - CreateCmd.Flags().StringVar(&iPassword, "password", "", "Password") -} diff --git a/cmd/user/user.go b/cmd/user/user.go deleted file mode 100644 index ce7f4231..00000000 --- a/cmd/user/user.go +++ /dev/null @@ -1,19 +0,0 @@ -package cmd - -import ( - "tinyauth/cmd/user/create" - "tinyauth/cmd/user/verify" - - "github.com/spf13/cobra" -) - -func UserCmd() *cobra.Command { - userCmd := &cobra.Command{ - Use: "user", - Short: "User utilities", - Long: `Utilities for creating and verifying tinyauth compatible users.`, - } - userCmd.AddCommand(create.CreateCmd) - userCmd.AddCommand(verify.VerifyCmd) - return userCmd -} diff --git a/cmd/user/verify/verify.go b/cmd/user/verify/verify.go deleted file mode 100644 index 4bff6ed1..00000000 --- a/cmd/user/verify/verify.go +++ /dev/null @@ -1,101 +0,0 @@ -package verify - -import ( - "errors" - "tinyauth/internal/utils" - - "github.com/charmbracelet/huh" - "github.com/pquerna/otp/totp" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - "golang.org/x/crypto/bcrypt" -) - -var interactive bool -var docker bool - -// i stands for input -var iUsername string -var iPassword string -var iTotp string -var iUser string - -var VerifyCmd = &cobra.Command{ - Use: "verify", - Short: "Verify a user is set up correctly", - Long: `Verify a user is set up correctly meaning that it has a correct username, password and totp code.`, - Run: func(cmd *cobra.Command, args []string) { - log.Logger = log.Level(zerolog.InfoLevel) - - if interactive { - form := huh.NewForm( - huh.NewGroup( - huh.NewInput().Title("User (username:hash:totp)").Value(&iUser).Validate((func(s string) error { - if s == "" { - return errors.New("user cannot be empty") - } - return nil - })), - huh.NewInput().Title("Username").Value(&iUsername).Validate((func(s string) error { - if s == "" { - return errors.New("username cannot be empty") - } - return nil - })), - huh.NewInput().Title("Password").Value(&iPassword).Validate((func(s string) error { - if s == "" { - return errors.New("password cannot be empty") - } - return nil - })), - huh.NewInput().Title("Totp Code (if setup)").Value(&iTotp), - ), - ) - var baseTheme *huh.Theme = huh.ThemeBase() - err := form.WithTheme(baseTheme).Run() - if err != nil { - log.Fatal().Err(err).Msg("Form failed") - } - } - - user, err := utils.ParseUser(iUser) - if err != nil { - log.Fatal().Err(err).Msg("Failed to parse user") - } - - if user.Username != iUsername { - log.Fatal().Msg("Username is incorrect") - } - - err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(iPassword)) - if err != nil { - log.Fatal().Msg("Password is incorrect") - } - - if user.TotpSecret == "" { - if iTotp != "" { - log.Warn().Msg("User does not have 2fa secret") - } - log.Info().Msg("User verified") - return - } - - ok := totp.Validate(iTotp, user.TotpSecret) - if !ok { - log.Fatal().Msg("Totp code incorrect") - - } - - log.Info().Msg("User verified") - }, -} - -func init() { - VerifyCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Create a user interactively") - VerifyCmd.Flags().BoolVar(&docker, "docker", false, "Is the user formatted for docker?") - VerifyCmd.Flags().StringVar(&iUsername, "username", "", "Username") - VerifyCmd.Flags().StringVar(&iPassword, "password", "", "Password") - VerifyCmd.Flags().StringVar(&iTotp, "totp", "", "Totp code") - VerifyCmd.Flags().StringVar(&iUser, "user", "", "Hash (username:hash:totp combination)") -} diff --git a/cmd/verify.go b/cmd/verify.go new file mode 100644 index 00000000..93b6a99e --- /dev/null +++ b/cmd/verify.go @@ -0,0 +1,118 @@ +package cmd + +import ( + "errors" + "tinyauth/internal/utils" + + "github.com/charmbracelet/huh" + "github.com/pquerna/otp/totp" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + "golang.org/x/crypto/bcrypt" +) + +type verifyUserCmd struct { + root *cobra.Command + cmd *cobra.Command + + interactive bool + username string + password string + totp string + user string +} + +func newVerifyUserCmd(root *cobra.Command) *verifyUserCmd { + return &verifyUserCmd{ + root: root, + } +} + +func (c *verifyUserCmd) Register() { + c.cmd = &cobra.Command{ + Use: "verify", + Short: "Verify a user is set up correctly", + Long: `Verify a user is set up correctly meaning that it has a correct username, password and TOTP code.`, + Run: c.run, + } + + c.cmd.Flags().BoolVarP(&c.interactive, "interactive", "i", false, "Validate a user interactively") + c.cmd.Flags().StringVar(&c.username, "username", "", "Username") + c.cmd.Flags().StringVar(&c.password, "password", "", "Password") + c.cmd.Flags().StringVar(&c.totp, "totp", "", "TOTP code") + c.cmd.Flags().StringVar(&c.user, "user", "", "Hash (username:hash:totp)") + + if c.root != nil { + c.root.AddCommand(c.cmd) + } +} + +func (c *verifyUserCmd) GetCmd() *cobra.Command { + return c.cmd +} + +func (c *verifyUserCmd) run(cmd *cobra.Command, args []string) { + log.Logger = log.Level(zerolog.InfoLevel) + + if c.interactive { + form := huh.NewForm( + huh.NewGroup( + huh.NewInput().Title("User (username:hash:totp)").Value(&c.user).Validate((func(s string) error { + if s == "" { + return errors.New("user cannot be empty") + } + return nil + })), + huh.NewInput().Title("Username").Value(&c.username).Validate((func(s string) error { + if s == "" { + return errors.New("username cannot be empty") + } + return nil + })), + huh.NewInput().Title("Password").Value(&c.password).Validate((func(s string) error { + if s == "" { + return errors.New("password cannot be empty") + } + return nil + })), + huh.NewInput().Title("TOTP Code (optional)").Value(&c.totp), + ), + ) + var baseTheme *huh.Theme = huh.ThemeBase() + err := form.WithTheme(baseTheme).Run() + if err != nil { + log.Fatal().Err(err).Msg("Form failed") + } + } + + user, err := utils.ParseUser(c.user) + if err != nil { + log.Fatal().Err(err).Msg("Failed to parse user") + } + + if user.Username != c.username { + log.Fatal().Msg("Username is incorrect") + } + + err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(c.password)) + if err != nil { + log.Fatal().Msg("Password is incorrect") + } + + if user.TotpSecret == "" { + if c.totp != "" { + log.Warn().Msg("User does not have TOTP secret") + } + log.Info().Msg("User verified") + return + } + + ok := totp.Validate(c.totp, user.TotpSecret) + if !ok { + log.Fatal().Msg("TOTP code incorrect") + + } + + log.Info().Msg("User verified") +} diff --git a/cmd/version.go b/cmd/version.go index 2a1827b7..37eb14a6 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -7,17 +7,36 @@ import ( "github.com/spf13/cobra" ) -var versionCmd = &cobra.Command{ - Use: "version", - Short: "Print the version number of Tinyauth", - Long: `All software has versions. This is Tinyauth's`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("Version: %s\n", config.Version) - fmt.Printf("Commit Hash: %s\n", config.CommitHash) - fmt.Printf("Build Timestamp: %s\n", config.BuildTimestamp) - }, +type versionCmd struct { + root *cobra.Command + cmd *cobra.Command } -func init() { - rootCmd.AddCommand(versionCmd) +func newVersionCmd(root *cobra.Command) *versionCmd { + return &versionCmd{ + root: root, + } +} + +func (c *versionCmd) Register() { + c.cmd = &cobra.Command{ + Use: "version", + Short: "Print the version number of Tinyauth", + Long: `All software has versions. This is Tinyauth's.`, + Run: c.run, + } + + if c.root != nil { + c.root.AddCommand(c.cmd) + } +} + +func (c *versionCmd) GetCmd() *cobra.Command { + return c.cmd +} + +func (c *versionCmd) run(cmd *cobra.Command, args []string) { + fmt.Printf("Version: %s\n", config.Version) + fmt.Printf("Commit Hash: %s\n", config.CommitHash) + fmt.Printf("Build Timestamp: %s\n", config.BuildTimestamp) } diff --git a/main.go b/main.go index 8126e9ed..36327494 100644 --- a/main.go +++ b/main.go @@ -11,5 +11,5 @@ import ( func main() { log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Caller().Logger() - cmd.Execute() + cmd.Run() } From f0a48cc91ce4d0c1cce96c15fbc34098059227df Mon Sep 17 00:00:00 2001 From: Stavros Date: Mon, 6 Oct 2025 21:45:23 +0300 Subject: [PATCH 050/127] feat: add health check command --- Dockerfile | 2 - cmd/health.go | 109 +++++++++++++++++++++++ cmd/root.go | 1 + internal/controller/health_controller.go | 4 +- 4 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 cmd/health.go diff --git a/Dockerfile b/Dockerfile index a0ebc0d0..f61a32a0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,8 +45,6 @@ FROM alpine:3.22 AS runner WORKDIR /tinyauth -RUN apk add --no-cache curl - COPY --from=builder /tinyauth/tinyauth ./ EXPOSE 3000 diff --git a/cmd/health.go b/cmd/health.go new file mode 100644 index 00000000..214076ef --- /dev/null +++ b/cmd/health.go @@ -0,0 +1,109 @@ +package cmd + +import ( + "encoding/json" + "errors" + "io" + "net/http" + "tinyauth/internal/config" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +type healthzResponse struct { + Status string `json:"status"` + Message string `json:"message"` +} + +type healthCmd struct { + root *cobra.Command + cmd *cobra.Command + + viper *viper.Viper + appUrl string +} + +func newHealthCmd(root *cobra.Command) *healthCmd { + return &healthCmd{ + root: root, + viper: viper.New(), + } +} + +func (c *healthCmd) Register() { + c.cmd = &cobra.Command{ + Use: "health", + Short: "Health check", + Long: `Use the health check endpoint to verify that Tinyauth is running and it's healthy.`, + Run: c.run, + } + + c.viper.AutomaticEnv() + c.cmd.Flags().StringVar(&c.appUrl, "app-url", "http://localhost:3000", "The URL where the Tinyauth server is running on.") + c.viper.BindEnv("app-url", "APP_URL") + c.viper.BindPFlags(c.cmd.Flags()) + + if c.root != nil { + c.root.AddCommand(c.cmd) + } +} + +func (c *healthCmd) GetCmd() *cobra.Command { + return c.cmd +} + +func (c *healthCmd) run(cmd *cobra.Command, args []string) { + log.Logger = log.Level(zerolog.InfoLevel) + + appUrl := c.viper.GetString("app-url") + + if appUrl == "" { + log.Fatal().Err(errors.New("app-url is required")).Msg("App URL is required") + } + + if config.Version == "development" { + log.Warn().Msg("Running in development mode. Overriding the app-url to http://localhost:3000") + appUrl = "http://localhost:3000" + } + + log.Info().Msgf("Health check endpoint is available at %s/api/healthz", appUrl) + + client := http.Client{} + + req, err := http.NewRequest("GET", appUrl+"/api/healthz", nil) + + if err != nil { + log.Fatal().Err(err).Msg("Failed to create request") + } + + resp, err := client.Do(req) + + if err != nil { + log.Fatal().Err(err).Msg("Failed to perform request") + } + + if resp.StatusCode != http.StatusOK { + log.Fatal().Err(errors.New("service is not healthy")).Msgf("Service is not healthy. Status code: %d", resp.StatusCode) + } + + defer resp.Body.Close() + + var healthResp healthzResponse + + body, err := io.ReadAll(resp.Body) + + if err != nil { + log.Fatal().Err(err).Msg("Failed to read response") + } + + err = json.Unmarshal(body, &healthResp) + + if err != nil { + log.Fatal().Err(err).Msg("Failed to decode response") + } + + log.Info().Interface("response", healthResp).Msg("Service is healthy") +} diff --git a/cmd/root.go b/cmd/root.go index e7bbb133..92abab3d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -140,6 +140,7 @@ func Run() { newVerifyUserCmd(userCmd).Register() newGenerateTotpCmd(totpCmd).Register() newVersionCmd(root).Register() + newHealthCmd(root).Register() root.AddCommand(userCmd) root.AddCommand(totpCmd) diff --git a/internal/controller/health_controller.go b/internal/controller/health_controller.go index 8f0aa424..8ad67b50 100644 --- a/internal/controller/health_controller.go +++ b/internal/controller/health_controller.go @@ -13,8 +13,8 @@ func NewHealthController(router *gin.RouterGroup) *HealthController { } func (controller *HealthController) SetupRoutes() { - controller.router.GET("/health", controller.healthHandler) - controller.router.HEAD("/health", controller.healthHandler) + controller.router.GET("/healthz", controller.healthHandler) + controller.router.HEAD("/healthz", controller.healthHandler) } func (controller *HealthController) healthHandler(c *gin.Context) { From a629430a888b5425f538e7a8772392dc374acb6b Mon Sep 17 00:00:00 2001 From: Stavros Date: Mon, 6 Oct 2025 21:46:15 +0300 Subject: [PATCH 051/127] chore: update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 48f731a5..761ea98c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@
Tinyauth

Tinyauth

-

The easiest way to secure your apps with a login screen.

+

The simplest way to protect your apps with a login screen.

@@ -14,7 +14,7 @@
-Tinyauth is a simple authentication middleware that adds a simple login screen or OAuth with Google, Github and any provider to all of your docker apps. It supports all the popular proxies like Traefik, Nginx and Caddy. +Tinyauth is a simple authentication middleware that adds a simple login screen or OAuth with Google, Github or any other provider to all of your apps. It supports all the popular proxies like Traefik, Nginx and Caddy. ![Screenshot](assets/screenshot.png) From 720f387908d58eb39f2da83fbd646cad9918f49f Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 7 Oct 2025 14:30:09 +0300 Subject: [PATCH 052/127] refactor: rework cli healthcheck logic --- cmd/health.go | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/cmd/health.go b/cmd/health.go index 214076ef..95508074 100644 --- a/cmd/health.go +++ b/cmd/health.go @@ -5,7 +5,6 @@ import ( "errors" "io" "net/http" - "tinyauth/internal/config" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -22,8 +21,7 @@ type healthCmd struct { root *cobra.Command cmd *cobra.Command - viper *viper.Viper - appUrl string + viper *viper.Viper } func newHealthCmd(root *cobra.Command) *healthCmd { @@ -42,9 +40,6 @@ func (c *healthCmd) Register() { } c.viper.AutomaticEnv() - c.cmd.Flags().StringVar(&c.appUrl, "app-url", "http://localhost:3000", "The URL where the Tinyauth server is running on.") - c.viper.BindEnv("app-url", "APP_URL") - c.viper.BindPFlags(c.cmd.Flags()) if c.root != nil { c.root.AddCommand(c.cmd) @@ -58,18 +53,18 @@ func (c *healthCmd) GetCmd() *cobra.Command { func (c *healthCmd) run(cmd *cobra.Command, args []string) { log.Logger = log.Level(zerolog.InfoLevel) - appUrl := c.viper.GetString("app-url") + appUrl := "http://127.0.0.1:3000" - if appUrl == "" { - log.Fatal().Err(errors.New("app-url is required")).Msg("App URL is required") - } + port := c.viper.GetString("PORT") + address := c.viper.GetString("ADDRESS") - if config.Version == "development" { - log.Warn().Msg("Running in development mode. Overriding the app-url to http://localhost:3000") - appUrl = "http://localhost:3000" + if address != "" && port != "" { + appUrl = "http://" + address + ":" + port } - log.Info().Msgf("Health check endpoint is available at %s/api/healthz", appUrl) + if len(args) > 0 { + appUrl = args[0] + } client := http.Client{} @@ -105,5 +100,5 @@ func (c *healthCmd) run(cmd *cobra.Command, args []string) { log.Fatal().Err(err).Msg("Failed to decode response") } - log.Info().Interface("response", healthResp).Msg("Service is healthy") + log.Info().Interface("response", healthResp).Msg("Tinyauth is healthy") } From 1ee0cee1714e424f4155362c9a294042e830e6eb Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 7 Oct 2025 15:03:53 +0300 Subject: [PATCH 053/127] feat: distroless image --- .github/workflows/nightly.yml | 159 +++++++++++++++++++++++++++++- Dockerfile | 4 +- Dockerfile.distroless | 56 +++++++++++ cmd/{health.go => healthcheck.go} | 30 +++--- cmd/root.go | 2 +- 5 files changed, 235 insertions(+), 16 deletions(-) create mode 100644 Dockerfile.distroless rename cmd/{health.go => healthcheck.go} (74%) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 75ae89f1..70f434d2 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -171,6 +171,8 @@ jobs: labels: ${{ steps.meta.outputs.labels }} tags: ghcr.io/${{ github.repository_owner }}/tinyauth outputs: type=image,push-by-digest=true,name-canonical=true,push=true + cache-from: type=gha + cache-to: type=gha,mode=max build-args: | VERSION=${{ needs.generate-metadata.outputs.VERSION }} COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }} @@ -190,6 +192,64 @@ jobs: if-no-files-found: error retention-days: 1 + image-build-distroless: + runs-on: ubuntu-latest + needs: + - create-release + - generate-metadata + - image-build + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: nightly + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository_owner }}/tinyauth + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push + uses: docker/build-push-action@v6 + id: build + with: + platforms: linux/amd64 + labels: ${{ steps.meta.outputs.labels }} + tags: ghcr.io/${{ github.repository_owner }}/tinyauth + outputs: type=image,push-by-digest=true,name-canonical=true,push=true + file: Dockerfile.distroless + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + VERSION=${{ needs.generate-metadata.outputs.VERSION }} + COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }} + BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }} + + - name: Export digest + run: | + mkdir -p ${{ runner.temp }}/digests + digest="${{ steps.build.outputs.digest }}" + touch "${{ runner.temp }}/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-distoless-linux-amd64 + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + image-build-arm: runs-on: ubuntu-24.04-arm needs: @@ -217,9 +277,62 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Set version + - name: Build and push + uses: docker/build-push-action@v6 + id: build + with: + platforms: linux/arm64 + labels: ${{ steps.meta.outputs.labels }} + tags: ghcr.io/${{ github.repository_owner }}/tinyauth + outputs: type=image,push-by-digest=true,name-canonical=true,push=true + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + VERSION=${{ needs.generate-metadata.outputs.VERSION }} + COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }} + BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }} + + - name: Export digest run: | - echo nightly > internal/assets/version + mkdir -p ${{ runner.temp }}/digests + digest="${{ steps.build.outputs.digest }}" + touch "${{ runner.temp }}/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-linux-arm64 + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + image-build-arm-distroless: + runs-on: ubuntu-24.04-arm + needs: + - create-release + - generate-metadata + - image-build-arm + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: nightly + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository_owner }}/tinyauth + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - name: Build and push uses: docker/build-push-action@v6 @@ -229,6 +342,9 @@ jobs: labels: ${{ steps.meta.outputs.labels }} tags: ghcr.io/${{ github.repository_owner }}/tinyauth outputs: type=image,push-by-digest=true,name-canonical=true,push=true + file: Dockerfile.distroless + cache-from: type=gha + cache-to: type=gha,mode=max build-args: | VERSION=${{ needs.generate-metadata.outputs.VERSION }} COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }} @@ -243,7 +359,7 @@ jobs: - name: Upload digest uses: actions/upload-artifact@v4 with: - name: digests-linux-arm64 + name: digests-distroless-linux-arm64 path: ${{ runner.temp }}/digests/* if-no-files-found: error retention-days: 1 @@ -285,6 +401,43 @@ jobs: docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ $(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *) + image-merge-distroless: + runs-on: ubuntu-latest + needs: + - image-build-distroless + - image-build-arm-distroless + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: ${{ runner.temp }}/digests + pattern: digests-distroless-* + merge-multiple: true + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository_owner }}/tinyauth + tags: | + type=raw,nightly-distroless + + - name: Create manifest list and push + working-directory: ${{ runner.temp }}/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *) + update-release: runs-on: ubuntu-latest needs: diff --git a/Dockerfile b/Dockerfile index f61a32a0..30056172 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,4 +51,6 @@ EXPOSE 3000 VOLUME ["/data"] -ENTRYPOINT ["./tinyauth"] \ No newline at end of file +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 CMD ["/tinyauth/tinyauth", "healthcheck"] + +ENTRYPOINT ["tinyauth"] \ No newline at end of file diff --git a/Dockerfile.distroless b/Dockerfile.distroless new file mode 100644 index 00000000..37723c66 --- /dev/null +++ b/Dockerfile.distroless @@ -0,0 +1,56 @@ +# Site builder +FROM oven/bun:1.2.23-alpine AS frontend-builder + +WORKDIR /frontend + +COPY ./frontend/package.json ./ +COPY ./frontend/bun.lock ./ + +RUN bun install + +COPY ./frontend/public ./public +COPY ./frontend/src ./src +COPY ./frontend/eslint.config.js ./ +COPY ./frontend/index.html ./ +COPY ./frontend/tsconfig.json ./ +COPY ./frontend/tsconfig.app.json ./ +COPY ./frontend/tsconfig.node.json ./ +COPY ./frontend/vite.config.ts ./ + +RUN bun run build + +# Builder +FROM golang:1.25-alpine3.21 AS builder + +ARG VERSION +ARG COMMIT_HASH +ARG BUILD_TIMESTAMP + +WORKDIR /tinyauth + +COPY go.mod ./ +COPY go.sum ./ + +RUN go mod download + +COPY ./main.go ./ +COPY ./cmd ./cmd +COPY ./internal ./internal +COPY --from=frontend-builder /frontend/dist ./internal/assets/dist + +RUN CGO_ENABLED=0 go build -ldflags "-s -w -X tinyauth/internal/config.Version=${VERSION} -X tinyauth/internal/config.CommitHash=${COMMIT_HASH} -X tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" + +# Runner +FROM gcr.io/distroless/static-debian12:latest AS runner + +WORKDIR /tinyauth + +COPY --from=builder /tinyauth/tinyauth ./ + +EXPOSE 3000 + +VOLUME ["/data"] + +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 CMD ["/tinyauth/tinyauth", "healthcheck"] + +ENTRYPOINT ["tinyauth"] \ No newline at end of file diff --git a/cmd/health.go b/cmd/healthcheck.go similarity index 74% rename from cmd/health.go rename to cmd/healthcheck.go index 95508074..7589afc6 100644 --- a/cmd/health.go +++ b/cmd/healthcheck.go @@ -17,24 +17,24 @@ type healthzResponse struct { Message string `json:"message"` } -type healthCmd struct { +type healthcheckCmd struct { root *cobra.Command cmd *cobra.Command viper *viper.Viper } -func newHealthCmd(root *cobra.Command) *healthCmd { - return &healthCmd{ +func newHealthcheckCmd(root *cobra.Command) *healthcheckCmd { + return &healthcheckCmd{ root: root, viper: viper.New(), } } -func (c *healthCmd) Register() { +func (c *healthcheckCmd) Register() { c.cmd = &cobra.Command{ - Use: "health", - Short: "Health check", + Use: "healthcheck [app-url]", + Short: "Perform a health check", Long: `Use the health check endpoint to verify that Tinyauth is running and it's healthy.`, Run: c.run, } @@ -46,26 +46,34 @@ func (c *healthCmd) Register() { } } -func (c *healthCmd) GetCmd() *cobra.Command { +func (c *healthcheckCmd) GetCmd() *cobra.Command { return c.cmd } -func (c *healthCmd) run(cmd *cobra.Command, args []string) { +func (c *healthcheckCmd) run(cmd *cobra.Command, args []string) { log.Logger = log.Level(zerolog.InfoLevel) - appUrl := "http://127.0.0.1:3000" + var appUrl string port := c.viper.GetString("PORT") address := c.viper.GetString("ADDRESS") - if address != "" && port != "" { - appUrl = "http://" + address + ":" + port + if port == "" { + port = "3000" } + if address == "" { + address = "127.0.0.1" + } + + appUrl = "http://" + address + ":" + port + if len(args) > 0 { appUrl = args[0] } + log.Info().Str("appUrl", appUrl).Msg("Performing health check") + client := http.Client{} req, err := http.NewRequest("GET", appUrl+"/api/healthz", nil) diff --git a/cmd/root.go b/cmd/root.go index 92abab3d..a0ce20d4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -140,7 +140,7 @@ func Run() { newVerifyUserCmd(userCmd).Register() newGenerateTotpCmd(totpCmd).Register() newVersionCmd(root).Register() - newHealthCmd(root).Register() + newHealthcheckCmd(root).Register() root.AddCommand(userCmd) root.AddCommand(totpCmd) From adb1a9bee5441f4872e650dff929f355d0e37fe3 Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 7 Oct 2025 15:15:21 +0300 Subject: [PATCH 054/127] fix: fix typo --- .github/workflows/nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 70f434d2..afc94f1b 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -245,7 +245,7 @@ jobs: - name: Upload digest uses: actions/upload-artifact@v4 with: - name: digests-distoless-linux-amd64 + name: digests-distroless-linux-amd64 path: ${{ runner.temp }}/digests/* if-no-files-found: error retention-days: 1 From 2cc6b6bdbbd7e60bcc38f2835517a279ef6bfb63 Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 7 Oct 2025 15:25:17 +0300 Subject: [PATCH 055/127] fix: tinyauth path in dockerfiles --- Dockerfile | 2 +- Dockerfile.distroless | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 30056172..f97af4eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,4 +53,4 @@ VOLUME ["/data"] HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 CMD ["/tinyauth/tinyauth", "healthcheck"] -ENTRYPOINT ["tinyauth"] \ No newline at end of file +ENTRYPOINT ["/tinyauth/tinyauth"] \ No newline at end of file diff --git a/Dockerfile.distroless b/Dockerfile.distroless index 37723c66..be009983 100644 --- a/Dockerfile.distroless +++ b/Dockerfile.distroless @@ -53,4 +53,4 @@ VOLUME ["/data"] HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 CMD ["/tinyauth/tinyauth", "healthcheck"] -ENTRYPOINT ["tinyauth"] \ No newline at end of file +ENTRYPOINT ["/tinyauth/tinyauth"] \ No newline at end of file From 103285855e61992f911e42868166d39bd3ce741c Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 7 Oct 2025 15:34:08 +0300 Subject: [PATCH 056/127] feat: add distroless to release workflow --- .github/workflows/nightly.yml | 4 + .github/workflows/release.yml | 157 ++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index afc94f1b..241dd7bc 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -173,6 +173,7 @@ jobs: outputs: type=image,push-by-digest=true,name-canonical=true,push=true cache-from: type=gha cache-to: type=gha,mode=max + github-token: ${{ secrets.GITHUB_TOKEN }} build-args: | VERSION=${{ needs.generate-metadata.outputs.VERSION }} COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }} @@ -231,6 +232,7 @@ jobs: file: Dockerfile.distroless cache-from: type=gha cache-to: type=gha,mode=max + github-token: ${{ secrets.GITHUB_TOKEN }} build-args: | VERSION=${{ needs.generate-metadata.outputs.VERSION }} COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }} @@ -287,6 +289,7 @@ jobs: outputs: type=image,push-by-digest=true,name-canonical=true,push=true cache-from: type=gha cache-to: type=gha,mode=max + github-token: ${{ secrets.GITHUB_TOKEN }} build-args: | VERSION=${{ needs.generate-metadata.outputs.VERSION }} COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }} @@ -345,6 +348,7 @@ jobs: file: Dockerfile.distroless cache-from: type=gha cache-to: type=gha,mode=max + github-token: ${{ secrets.GITHUB_TOKEN }} build-args: | VERSION=${{ needs.generate-metadata.outputs.VERSION }} COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1a1bd25e..58711b37 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -143,6 +143,9 @@ jobs: labels: ${{ steps.meta.outputs.labels }} tags: ghcr.io/${{ github.repository_owner }}/tinyauth outputs: type=image,push-by-digest=true,name-canonical=true,push=true + cache-from: type=gha + cache-to: type=gha,mode=max + github-token: ${{ secrets.GITHUB_TOKEN }} build-args: | VERSION=${{ needs.generate-metadata.outputs.VERSION }} COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }} @@ -162,6 +165,62 @@ jobs: if-no-files-found: error retention-days: 1 + image-build-distroless: + runs-on: ubuntu-latest + needs: + - generate-metadata + - image-build + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository_owner }}/tinyauth + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push + uses: docker/build-push-action@v6 + id: build + with: + platforms: linux/amd64 + labels: ${{ steps.meta.outputs.labels }} + tags: ghcr.io/${{ github.repository_owner }}/tinyauth + outputs: type=image,push-by-digest=true,name-canonical=true,push=true + file: Dockerfile.distroless + cache-from: type=gha + cache-to: type=gha,mode=max + github-token: ${{ secrets.GITHUB_TOKEN }} + build-args: | + VERSION=${{ needs.generate-metadata.outputs.VERSION }} + COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }} + BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }} + + - name: Export digest + run: | + mkdir -p ${{ runner.temp }}/digests + digest="${{ steps.build.outputs.digest }}" + touch "${{ runner.temp }}/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-distroless-linux-amd64 + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + image-build-arm: runs-on: ubuntu-24.04-arm needs: @@ -194,6 +253,9 @@ jobs: labels: ${{ steps.meta.outputs.labels }} tags: ghcr.io/${{ github.repository_owner }}/tinyauth outputs: type=image,push-by-digest=true,name-canonical=true,push=true + cache-from: type=gha + cache-to: type=gha,mode=max + github-token: ${{ secrets.GITHUB_TOKEN }} build-args: | VERSION=${{ needs.generate-metadata.outputs.VERSION }} COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }} @@ -213,6 +275,62 @@ jobs: if-no-files-found: error retention-days: 1 + image-build-arm-distroless: + runs-on: ubuntu-24.04-arm + needs: + - generate-metadata + - image-build-arm + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository_owner }}/tinyauth + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push + uses: docker/build-push-action@v6 + id: build + with: + platforms: linux/arm64 + labels: ${{ steps.meta.outputs.labels }} + tags: ghcr.io/${{ github.repository_owner }}/tinyauth + outputs: type=image,push-by-digest=true,name-canonical=true,push=true + file: Dockerfile.distroless + cache-from: type=gha + cache-to: type=gha,mode=max + github-token: ${{ secrets.GITHUB_TOKEN }} + build-args: | + VERSION=${{ needs.generate-metadata.outputs.VERSION }} + COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }} + BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }} + + - name: Export digest + run: | + mkdir -p ${{ runner.temp }}/digests + digest="${{ steps.build.outputs.digest }}" + touch "${{ runner.temp }}/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-distroless-linux-arm64 + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + image-merge: runs-on: ubuntu-latest needs: @@ -252,6 +370,45 @@ jobs: docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ $(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *) + image-merge-distroless: + runs-on: ubuntu-latest + needs: + - image-build-distroless + - image-build-arm-distroless + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: ${{ runner.temp }}/digests + pattern: digests-distroless-* + merge-multiple: true + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository_owner }}/tinyauth + tags: | + type=semver,pattern={{version}},prefix=v,suffix=-distroless + type=semver,pattern={{major}},prefix=v,suffix=-distroless + type=semver,pattern={{major}}.{{minor}},prefix=v,suffix=-distroless + + - name: Create manifest list and push + working-directory: ${{ runner.temp }}/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *) + update-release: runs-on: ubuntu-latest needs: From bbf8112995f613d15d63b12b348359774ab654e8 Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 7 Oct 2025 15:41:39 +0300 Subject: [PATCH 057/127] refactor: touch up loggin in healthcheck command --- cmd/healthcheck.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/healthcheck.go b/cmd/healthcheck.go index 7589afc6..ca2bd839 100644 --- a/cmd/healthcheck.go +++ b/cmd/healthcheck.go @@ -72,7 +72,7 @@ func (c *healthcheckCmd) run(cmd *cobra.Command, args []string) { appUrl = args[0] } - log.Info().Str("appUrl", appUrl).Msg("Performing health check") + log.Info().Str("app_url", appUrl).Msg("Performing health check") client := http.Client{} From c337ba5b3140007b294c0db3e88acec627fb2942 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 15:42:18 +0300 Subject: [PATCH 058/127] chore(deps): bump github.com/go-playground/validator/v10 (#387) Bumps the minor-patch group with 1 update: [github.com/go-playground/validator/v10](https://github.com/go-playground/validator). Updates `github.com/go-playground/validator/v10` from 10.27.0 to 10.28.0 - [Release notes](https://github.com/go-playground/validator/releases) - [Commits](https://github.com/go-playground/validator/compare/v10.27.0...v10.28.0) --- updated-dependencies: - dependency-name: github.com/go-playground/validator/v10 dependency-version: 10.28.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 74ad6fb1..17216945 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 github.com/gin-gonic/gin v1.11.0 github.com/glebarez/sqlite v1.11.0 - github.com/go-playground/validator/v10 v10.27.0 + github.com/go-playground/validator/v10 v10.28.0 github.com/golang-migrate/migrate/v4 v4.19.0 github.com/google/go-querystring v1.1.0 github.com/google/uuid v1.6.0 @@ -87,7 +87,7 @@ require ( github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-ldap/ldap/v3 v3.4.12 github.com/go-logr/logr v1.4.3 // indirect diff --git a/go.sum b/go.sum index de35e1b7..d6690a6b 100644 --- a/go.sum +++ b/go.sum @@ -88,8 +88,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= +github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= @@ -113,8 +113,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= -github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= From 9ba1695274bca9b59ac8a53fd15ce3271e3406ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 15:43:16 +0300 Subject: [PATCH 059/127] chore(deps-dev): bump eslint-plugin-react-hooks in /frontend (#388) Bumps [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) from 5.2.0 to 6.1.1. - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/HEAD/packages/eslint-plugin-react-hooks) --- updated-dependencies: - dependency-name: eslint-plugin-react-hooks dependency-version: 6.1.1 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/bun.lock | 6 ++++-- frontend/package.json | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/bun.lock b/frontend/bun.lock index cfa15db7..058e5f9e 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -39,7 +39,7 @@ "@types/react-dom": "^19.2.0", "@vitejs/plugin-react": "^5.0.4", "eslint": "^9.36.0", - "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-hooks": "^6.1.1", "eslint-plugin-react-refresh": "^0.4.23", "globals": "^16.4.0", "prettier": "3.6.2", @@ -493,7 +493,7 @@ "eslint": ["eslint@9.36.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.36.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ=="], - "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@6.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "zod": "^3.22.4 || ^4.0.0", "zod-validation-error": "^3.0.3 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-St9EKZzOAQF704nt2oJvAKZHjhrpg25ClQoaAlHmPZuajFldVLqRDW4VBNAS01NzeiQF0m0qhG1ZA807K6aVaQ=="], "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.23", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-G4j+rv0NmbIR45kni5xJOrYvCtyD3/7LjpVH8MPPcudXDcNu8gv+4ATTDXTtbRR8rTCM5HxECvCSsRmxKnWDsA=="], @@ -909,6 +909,8 @@ "zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="], + "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], "@babel/generator/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="], diff --git a/frontend/package.json b/frontend/package.json index 4d7b4597..917f5c77 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -45,7 +45,7 @@ "@types/react-dom": "^19.2.0", "@vitejs/plugin-react": "^5.0.4", "eslint": "^9.36.0", - "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-hooks": "^6.1.1", "eslint-plugin-react-refresh": "^0.4.23", "globals": "^16.4.0", "prettier": "3.6.2", From 4a530eebc9cb272aa4749aa8693be808182b8c68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 15:44:33 +0300 Subject: [PATCH 060/127] chore(deps): bump react-i18next from 15.7.3 to 16.0.0 in /frontend (#389) Bumps [react-i18next](https://github.com/i18next/react-i18next) from 15.7.3 to 16.0.0. - [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/react-i18next/compare/v15.7.3...v16.0.0) --- updated-dependencies: - dependency-name: react-i18next dependency-version: 16.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/bun.lock | 4 ++-- frontend/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/bun.lock b/frontend/bun.lock index 058e5f9e..44fd5774 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -23,7 +23,7 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "react-hook-form": "^7.63.0", - "react-i18next": "^15.7.3", + "react-i18next": "^16.0.0", "react-markdown": "^10.1.0", "react-router": "^7.9.3", "sonner": "^2.0.7", @@ -789,7 +789,7 @@ "react-hook-form": ["react-hook-form@7.63.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA=="], - "react-i18next": ["react-i18next@15.7.3", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 25.4.1", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-AANws4tOE+QSq/IeMF/ncoHlMNZaVLxpa5uUGW1wjike68elVYr0018L9xYoqBr1OFO7G7boDPrbn0HpMCJxTw=="], + "react-i18next": ["react-i18next@16.0.0", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 25.5.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-JQ+dFfLnFSKJQt7W01lJHWRC0SX7eDPobI+MSTJ3/gP39xH2g33AuTE7iddAfXYHamJdAeMGM0VFboPaD3G68Q=="], "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], diff --git a/frontend/package.json b/frontend/package.json index 917f5c77..3e0f71c0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,7 +29,7 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "react-hook-form": "^7.63.0", - "react-i18next": "^15.7.3", + "react-i18next": "^16.0.0", "react-markdown": "^10.1.0", "react-router": "^7.9.3", "sonner": "^2.0.7", From cbe31d442db2995a1a065cfc6ec9445d6ad9c53c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 15:49:41 +0300 Subject: [PATCH 061/127] chore(deps): bump the minor-patch group across 1 directory with 10 updates (#391) Bumps the minor-patch group with 10 updates in the /frontend directory: | Package | From | To | | --- | --- | --- | | [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.544.0` | `0.545.0` | | [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.63.0` | `7.64.0` | | [zod](https://github.com/colinhacks/zod) | `4.1.11` | `4.1.12` | | [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) | `9.36.0` | `9.37.0` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `24.6.2` | `24.7.0` | | [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.0` | `19.2.2` | | [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `19.2.0` | `19.2.1` | | [eslint](https://github.com/eslint/eslint) | `9.36.0` | `9.37.0` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.45.0` | `8.46.0` | | [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.1.8` | `7.1.9` | Updates `lucide-react` from 0.544.0 to 0.545.0 - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/0.545.0/packages/lucide-react) Updates `react-hook-form` from 7.63.0 to 7.64.0 - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.63.0...v7.64.0) Updates `zod` from 4.1.11 to 4.1.12 - [Release notes](https://github.com/colinhacks/zod/releases) - [Commits](https://github.com/colinhacks/zod/compare/v4.1.11...v4.1.12) Updates `@eslint/js` from 9.36.0 to 9.37.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/commits/v9.37.0/packages/js) Updates `@types/node` from 24.6.2 to 24.7.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/react` from 19.2.0 to 19.2.2 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react) Updates `@types/react-dom` from 19.2.0 to 19.2.1 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom) Updates `eslint` from 9.36.0 to 9.37.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v9.36.0...v9.37.0) Updates `typescript-eslint` from 8.45.0 to 8.46.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.46.0/packages/typescript-eslint) Updates `vite` from 7.1.8 to 7.1.9 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v7.1.9/packages/vite) --- updated-dependencies: - dependency-name: lucide-react dependency-version: 0.545.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: react-hook-form dependency-version: 7.64.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: zod dependency-version: 4.1.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: "@eslint/js" dependency-version: 9.37.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@types/node" dependency-version: 24.7.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@types/react" dependency-version: 19.2.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: "@types/react-dom" dependency-version: 19.2.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: eslint dependency-version: 9.37.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: typescript-eslint dependency-version: 8.46.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: vite dependency-version: 7.1.9 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/bun.lock | 110 ++++++++++++++++++++++++++++++------------ frontend/package.json | 20 ++++---- 2 files changed, 89 insertions(+), 41 deletions(-) diff --git a/frontend/bun.lock b/frontend/bun.lock index 44fd5774..d74b1d81 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -18,35 +18,35 @@ "i18next-browser-languagedetector": "^8.2.0", "i18next-resources-to-backend": "^1.2.1", "input-otp": "^1.4.2", - "lucide-react": "^0.544.0", + "lucide-react": "^0.545.0", "next-themes": "^0.4.6", "react": "^19.2.0", "react-dom": "^19.2.0", - "react-hook-form": "^7.63.0", + "react-hook-form": "^7.64.0", "react-i18next": "^16.0.0", "react-markdown": "^10.1.0", "react-router": "^7.9.3", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.14", - "zod": "^4.1.11", + "zod": "^4.1.12", }, "devDependencies": { - "@eslint/js": "^9.36.0", + "@eslint/js": "^9.37.0", "@tanstack/eslint-plugin-query": "^5.91.0", - "@types/node": "^24.6.2", - "@types/react": "^19.2.0", - "@types/react-dom": "^19.2.0", + "@types/node": "^24.7.0", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.1", "@vitejs/plugin-react": "^5.0.4", - "eslint": "^9.36.0", + "eslint": "^9.37.0", "eslint-plugin-react-hooks": "^6.1.1", "eslint-plugin-react-refresh": "^0.4.23", "globals": "^16.4.0", "prettier": "3.6.2", "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", - "typescript-eslint": "^8.45.0", - "vite": "^7.1.8", + "typescript-eslint": "^8.46.0", + "vite": "^7.1.9", }, }, }, @@ -147,17 +147,17 @@ "@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="], - "@eslint/config-helpers": ["@eslint/config-helpers@0.3.1", "", {}, "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA=="], + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.0", "", { "dependencies": { "@eslint/core": "^0.16.0" } }, "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog=="], - "@eslint/core": ["@eslint/core@0.15.2", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg=="], + "@eslint/core": ["@eslint/core@0.16.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q=="], "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], - "@eslint/js": ["@eslint/js@9.36.0", "", {}, "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw=="], + "@eslint/js": ["@eslint/js@9.37.0", "", {}, "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg=="], "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.5", "", { "dependencies": { "@eslint/core": "^0.15.2", "levn": "^0.4.1" } }, "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w=="], + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.0", "", { "dependencies": { "@eslint/core": "^0.16.0", "levn": "^0.4.1" } }, "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A=="], "@floating-ui/core": ["@floating-ui/core@1.7.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA=="], @@ -355,33 +355,33 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@24.6.2", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang=="], + "@types/node": ["@types/node@24.7.0", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw=="], - "@types/react": ["@types/react@19.2.0", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA=="], + "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], - "@types/react-dom": ["@types/react-dom@19.2.0", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg=="], + "@types/react-dom": ["@types/react-dom@19.2.1", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.45.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/type-utils": "8.45.0", "@typescript-eslint/utils": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.45.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.46.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/type-utils": "8.46.0", "@typescript-eslint/utils": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.46.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.45.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/types": "8.45.0", "@typescript-eslint/typescript-estree": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.46.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", "@typescript-eslint/typescript-estree": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.45.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.45.0", "@typescript-eslint/types": "^8.45.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.46.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.46.0", "@typescript-eslint/types": "^8.46.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ=="], "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.45.0", "", { "dependencies": { "@typescript-eslint/types": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0" } }, "sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.45.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.46.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.45.0", "", { "dependencies": { "@typescript-eslint/types": "8.45.0", "@typescript-eslint/typescript-estree": "8.45.0", "@typescript-eslint/utils": "8.45.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.46.0", "", { "dependencies": { "@typescript-eslint/types": "8.46.0", "@typescript-eslint/typescript-estree": "8.46.0", "@typescript-eslint/utils": "8.46.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg=="], "@typescript-eslint/types": ["@typescript-eslint/types@8.45.0", "", {}, "sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.45.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.45.0", "@typescript-eslint/tsconfig-utils": "8.45.0", "@typescript-eslint/types": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.46.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.46.0", "@typescript-eslint/tsconfig-utils": "8.46.0", "@typescript-eslint/types": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg=="], "@typescript-eslint/utils": ["@typescript-eslint/utils@8.45.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/types": "8.45.0", "@typescript-eslint/typescript-estree": "8.45.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.45.0", "", { "dependencies": { "@typescript-eslint/types": "8.45.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.46.0", "", { "dependencies": { "@typescript-eslint/types": "8.46.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], @@ -491,7 +491,7 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@9.36.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.36.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ=="], + "eslint": ["eslint@9.37.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.4.0", "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.37.0", "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig=="], "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@6.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "zod": "^3.22.4 || ^4.0.0", "zod-validation-error": "^3.0.3 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-St9EKZzOAQF704nt2oJvAKZHjhrpg25ClQoaAlHmPZuajFldVLqRDW4VBNAS01NzeiQF0m0qhG1ZA807K6aVaQ=="], @@ -663,7 +663,7 @@ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "lucide-react": ["lucide-react@0.544.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw=="], + "lucide-react": ["lucide-react@0.545.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw=="], "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], @@ -787,7 +787,7 @@ "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], - "react-hook-form": ["react-hook-form@7.63.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA=="], + "react-hook-form": ["react-hook-form@7.64.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-fnN+vvTiMLnRqKNTVhDysdrUay0kUUAymQnFIznmgDvapjveUWOOPqMNzPg+A+0yf9DuE2h6xzBjN1s+Qx8wcg=="], "react-i18next": ["react-i18next@16.0.0", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 25.5.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-JQ+dFfLnFSKJQt7W01lJHWRC0SX7eDPobI+MSTJ3/gP39xH2g33AuTE7iddAfXYHamJdAeMGM0VFboPaD3G68Q=="], @@ -867,9 +867,9 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "typescript-eslint": ["typescript-eslint@8.45.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.45.0", "@typescript-eslint/parser": "8.45.0", "@typescript-eslint/typescript-estree": "8.45.0", "@typescript-eslint/utils": "8.45.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qzDmZw/Z5beNLUrXfd0HIW6MzIaAV5WNDxmMs9/3ojGOpYavofgNAAD/nC6tGV2PczIi0iw8vot2eAe/sBn7zg=="], + "typescript-eslint": ["typescript-eslint@8.46.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.46.0", "@typescript-eslint/parser": "8.46.0", "@typescript-eslint/typescript-estree": "8.46.0", "@typescript-eslint/utils": "8.46.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-6+ZrB6y2bT2DX3K+Qd9vn7OFOJR+xSLDj+Aw/N3zBwUt27uTw2sw2TE2+UcY1RiyBZkaGbTkVg9SSdPNUG6aUw=="], - "undici-types": ["undici-types@7.13.0", "", {}, "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ=="], + "undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], @@ -895,7 +895,7 @@ "vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="], - "vite": ["vite@7.1.8", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-oBXvfSHEOL8jF+R9Am7h59Up07kVVGH1NrFGFoEG6bPDZP3tGpQhvkBpy5x7U6+E6wZCu9OihsWgJqDbQIm8LQ=="], + "vite": ["vite@7.1.9", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg=="], "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], @@ -907,7 +907,7 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="], + "zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="], "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], @@ -967,12 +967,36 @@ "@types/estree-jsx/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.0", "", { "dependencies": { "@typescript-eslint/types": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0" } }, "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw=="], + + "@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", "@typescript-eslint/typescript-estree": "8.46.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.4", "", {}, "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="], + "@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.0", "", { "dependencies": { "@typescript-eslint/types": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0" } }, "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw=="], + + "@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="], + + "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="], + + "@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.45.0", "", { "dependencies": { "@typescript-eslint/types": "8.45.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag=="], + + "@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="], + + "@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", "@typescript-eslint/typescript-estree": "8.46.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g=="], + + "@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="], + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], + "@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.45.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.45.0", "@typescript-eslint/tsconfig-utils": "8.45.0", "@typescript-eslint/types": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA=="], + + "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="], + + "eslint-plugin-react-hooks/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="], + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "hast-util-to-jsx-runtime/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], @@ -987,6 +1011,8 @@ "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + "typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", "@typescript-eslint/typescript-estree": "8.46.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g=="], + "@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.27.1", "", { "dependencies": { "@babel/parser": "^7.27.1", "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w=="], "@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.27.2", "", { "dependencies": { "@babel/types": "^7.27.1" }, "bin": "./bin/babel-parser.js" }, "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw=="], @@ -1001,12 +1027,34 @@ "@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="], + + "@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="], + + "@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.0", "", { "dependencies": { "@typescript-eslint/types": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0" } }, "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.45.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.45.0", "@typescript-eslint/types": "^8.45.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg=="], + + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.45.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w=="], + + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.45.0", "", { "dependencies": { "@typescript-eslint/types": "8.45.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag=="], + + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], + + "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.0", "", { "dependencies": { "@typescript-eslint/types": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0" } }, "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw=="], + + "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="], + "@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], "@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + "@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], "@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], diff --git a/frontend/package.json b/frontend/package.json index 3e0f71c0..e0a270f1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,34 +24,34 @@ "i18next-browser-languagedetector": "^8.2.0", "i18next-resources-to-backend": "^1.2.1", "input-otp": "^1.4.2", - "lucide-react": "^0.544.0", + "lucide-react": "^0.545.0", "next-themes": "^0.4.6", "react": "^19.2.0", "react-dom": "^19.2.0", - "react-hook-form": "^7.63.0", + "react-hook-form": "^7.64.0", "react-i18next": "^16.0.0", "react-markdown": "^10.1.0", "react-router": "^7.9.3", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.14", - "zod": "^4.1.11" + "zod": "^4.1.12" }, "devDependencies": { - "@eslint/js": "^9.36.0", + "@eslint/js": "^9.37.0", "@tanstack/eslint-plugin-query": "^5.91.0", - "@types/node": "^24.6.2", - "@types/react": "^19.2.0", - "@types/react-dom": "^19.2.0", + "@types/node": "^24.7.0", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.1", "@vitejs/plugin-react": "^5.0.4", - "eslint": "^9.36.0", + "eslint": "^9.37.0", "eslint-plugin-react-hooks": "^6.1.1", "eslint-plugin-react-refresh": "^0.4.23", "globals": "^16.4.0", "prettier": "3.6.2", "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", - "typescript-eslint": "^8.45.0", - "vite": "^7.1.8" + "typescript-eslint": "^8.46.0", + "vite": "^7.1.9" } } \ No newline at end of file From adffb4ac0ad0c806a303dd452931263241a08245 Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 8 Oct 2025 15:15:30 +0300 Subject: [PATCH 062/127] fix: names in oauth broker --- internal/bootstrap/app_bootstrap.go | 12 ------------ internal/service/oauth_broker_service.go | 2 +- internal/utils/app_utils.go | 13 ++++++++++++- internal/utils/app_utils_test.go | 4 ++++ 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index b9c38307..5f9b9064 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -150,18 +150,6 @@ func (app *BootstrapApp) Setup() error { configuredProviders := make([]controller.Provider, 0) for id, provider := range oauthProviders { - if id == "" { - continue - } - - if provider.Name == "" { - if name, ok := config.OverrideProviders[id]; ok { - provider.Name = name - } else { - provider.Name = utils.Capitalize(id) - } - } - configuredProviders = append(configuredProviders, controller.Provider{ Name: provider.Name, ID: id, diff --git a/internal/service/oauth_broker_service.go b/internal/service/oauth_broker_service.go index 178f9af9..10381841 100644 --- a/internal/service/oauth_broker_service.go +++ b/internal/service/oauth_broker_service.go @@ -50,7 +50,7 @@ func (broker *OAuthBrokerService) Init() error { log.Error().Err(err).Msgf("Failed to initialize OAuth service: %T", name) return err } - log.Info().Str("service", service.GetName()).Msg("Initialized OAuth service") + log.Info().Str("service", name).Msg("Initialized OAuth service") } return nil diff --git a/internal/utils/app_utils.go b/internal/utils/app_utils.go index 42649cd8..de0cff63 100644 --- a/internal/utils/app_utils.go +++ b/internal/utils/app_utils.go @@ -184,7 +184,6 @@ func GetOAuthProvidersConfig(env []string, args []string, appUrl string) (map[st } // If we have google/github providers and no redirect URL then set a default - for id := range config.OverrideProviders { if provider, exists := providers[id]; exists { if provider.RedirectURL == "" { @@ -194,6 +193,18 @@ func GetOAuthProvidersConfig(env []string, args []string, appUrl string) (map[st } } + // Set names + for id, provider := range providers { + if provider.Name == "" { + if name, ok := config.OverrideProviders[id]; ok { + provider.Name = name + } else { + provider.Name = Capitalize(id) + } + } + providers[id] = provider + } + // Return combined providers return providers, nil } diff --git a/internal/utils/app_utils_test.go b/internal/utils/app_utils_test.go index a7f09fe6..d8abcab4 100644 --- a/internal/utils/app_utils_test.go +++ b/internal/utils/app_utils_test.go @@ -210,10 +210,12 @@ func TestGetOAuthProvidersConfig(t *testing.T) { "client1": { ClientID: "client1-id", ClientSecret: "client1-secret", + Name: "Client1", }, "client2": { ClientID: "client2-id", ClientSecret: "client2-secret", + Name: "Client2", }, } @@ -247,6 +249,7 @@ func TestGetOAuthProvidersConfig(t *testing.T) { "client1": { ClientID: "client1-id", ClientSecret: "file content", + Name: "Client1", }, } @@ -262,6 +265,7 @@ func TestGetOAuthProvidersConfig(t *testing.T) { ClientID: "google-id", ClientSecret: "google-secret", RedirectURL: "http://app.url/api/oauth/callback/google", + Name: "Google", }, } From c5d70d7c93bbc8a40e49ddb713ff6278d4605eda Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 8 Oct 2025 15:35:54 +0300 Subject: [PATCH 063/127] feat: set page title based on configured title --- frontend/src/components/layout/layout.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/layout/layout.tsx b/frontend/src/components/layout/layout.tsx index 3461000b..e7780cd7 100644 --- a/frontend/src/components/layout/layout.tsx +++ b/frontend/src/components/layout/layout.tsx @@ -1,11 +1,17 @@ import { useAppContext } from "@/context/app-context"; import { LanguageSelector } from "../language/language"; import { Outlet } from "react-router"; -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { DomainWarning } from "../domain-warning/domain-warning"; const BaseLayout = ({ children }: { children: React.ReactNode }) => { - const { backgroundImage } = useAppContext(); + const { backgroundImage, title } = useAppContext(); + + useEffect(() => { + if (title !== "Tinyauth") { + document.title = title + " - Tinyauth"; + } + }, [title]); return (
Date: Wed, 8 Oct 2025 15:42:52 +0300 Subject: [PATCH 064/127] refactor: use non-google specific meta tags --- frontend/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/index.html b/frontend/index.html index 42e1f81e..8078a279 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -8,7 +8,7 @@ - + Tinyauth From 287c6f975fbc4044de10ac2ac21ba6756a61b49f Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 8 Oct 2025 15:44:34 +0300 Subject: [PATCH 065/127] fix: do not allow request if docker labels check fail --- internal/service/docker_service.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/internal/service/docker_service.go b/internal/service/docker_service.go index d2a4cfc4..a23af1aa 100644 --- a/internal/service/docker_service.go +++ b/internal/service/docker_service.go @@ -71,14 +71,12 @@ func (docker *DockerService) GetLabels(appDomain string) (config.App, error) { for _, ctr := range containers { inspect, err := docker.InspectContainer(ctr.ID) if err != nil { - log.Warn().Str("id", ctr.ID).Err(err).Msg("Error inspecting container, skipping") - continue + return config.App{}, err } labels, err := decoders.DecodeLabels(inspect.Config.Labels) if err != nil { - log.Warn().Str("id", ctr.ID).Err(err).Msg("Error getting container labels, skipping") - continue + return config.App{}, err } for appName, appLabels := range labels.Apps { From c77da30d87ce7fb3e17054d1e52858bee8d10b84 Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 8 Oct 2025 16:24:14 +0300 Subject: [PATCH 066/127] refactor: set gin mode using env --- Dockerfile | 2 ++ Dockerfile.distroless | 2 ++ internal/bootstrap/app_bootstrap.go | 4 ---- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index f97af4eb..a34c499f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,4 +53,6 @@ VOLUME ["/data"] HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 CMD ["/tinyauth/tinyauth", "healthcheck"] +ENV GIN_MODE=release + ENTRYPOINT ["/tinyauth/tinyauth"] \ No newline at end of file diff --git a/Dockerfile.distroless b/Dockerfile.distroless index be009983..146792f2 100644 --- a/Dockerfile.distroless +++ b/Dockerfile.distroless @@ -53,4 +53,6 @@ VOLUME ["/data"] HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 CMD ["/tinyauth/tinyauth", "healthcheck"] +ENV GIN_MODE=release + ENTRYPOINT ["/tinyauth/tinyauth"] \ No newline at end of file diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index 5f9b9064..d3dbfc2d 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -172,10 +172,6 @@ func (app *BootstrapApp) Setup() error { } // Create engine - if config.Version != "development" { - gin.SetMode(gin.ReleaseMode) - } - engine := gin.New() if len(app.config.TrustedProxies) > 0 { From e5f1df03c4afd5dad2fc55cfbfd7c07ccba5e4d8 Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 8 Oct 2025 16:40:22 +0300 Subject: [PATCH 067/127] feat: add tinyauth to container path --- Dockerfile | 8 +++++--- Dockerfile.distroless | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index a34c499f..1bbee231 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,8 +51,10 @@ EXPOSE 3000 VOLUME ["/data"] -HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 CMD ["/tinyauth/tinyauth", "healthcheck"] - ENV GIN_MODE=release -ENTRYPOINT ["/tinyauth/tinyauth"] \ No newline at end of file +ENV PATH=$PATH:/tinyauth + +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 CMD ["tinyauth", "healthcheck"] + +ENTRYPOINT ["tinyauth"] \ No newline at end of file diff --git a/Dockerfile.distroless b/Dockerfile.distroless index 146792f2..068f7550 100644 --- a/Dockerfile.distroless +++ b/Dockerfile.distroless @@ -51,8 +51,10 @@ EXPOSE 3000 VOLUME ["/data"] -HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 CMD ["/tinyauth/tinyauth", "healthcheck"] - ENV GIN_MODE=release -ENTRYPOINT ["/tinyauth/tinyauth"] \ No newline at end of file +ENV PATH=$PATH:/tinyauth + +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 CMD ["tinyauth", "healthcheck"] + +ENTRYPOINT ["tinyauth"] \ No newline at end of file From e8785161304d1197fd9203a4672b0aceee781415 Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 8 Oct 2025 16:41:41 +0300 Subject: [PATCH 068/127] refactor: don't add tinyauth suffix to title --- frontend/src/components/layout/layout.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/components/layout/layout.tsx b/frontend/src/components/layout/layout.tsx index e7780cd7..8f49e726 100644 --- a/frontend/src/components/layout/layout.tsx +++ b/frontend/src/components/layout/layout.tsx @@ -8,9 +8,7 @@ const BaseLayout = ({ children }: { children: React.ReactNode }) => { const { backgroundImage, title } = useAppContext(); useEffect(() => { - if (title !== "Tinyauth") { - document.title = title + " - Tinyauth"; - } + document.title = title; }, [title]); return ( From 5b7bda33782ec67256b405f60a9eb5b7e3ea5ea0 Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 8 Oct 2025 18:56:42 +0300 Subject: [PATCH 069/127] chore: use bun frozen lockfile for builds --- .github/workflows/ci.yml | 2 +- .github/workflows/nightly.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- Dockerfile | 2 +- Dockerfile.distroless | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba634195..c2b5f5e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: - name: Install frontend dependencies run: | cd frontend - bun install + bun install --frozen-lockfile - name: Set version run: | diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 241dd7bc..bb3c859b 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -66,7 +66,7 @@ jobs: - name: Install frontend dependencies run: | cd frontend - bun install + bun install --frozen-lockfile - name: Install backend dependencies run: | @@ -112,7 +112,7 @@ jobs: - name: Install frontend dependencies run: | cd frontend - bun install + bun install --frozen-lockfile - name: Install backend dependencies run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 58711b37..9d09e04a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,7 +44,7 @@ jobs: - name: Install frontend dependencies run: | cd frontend - bun install + bun install --frozen-lockfile - name: Install backend dependencies run: | @@ -87,7 +87,7 @@ jobs: - name: Install frontend dependencies run: | cd frontend - bun install + bun install --frozen-lockfile - name: Install backend dependencies run: | diff --git a/Dockerfile b/Dockerfile index 1bbee231..fbd280b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ WORKDIR /frontend COPY ./frontend/package.json ./ COPY ./frontend/bun.lock ./ -RUN bun install +RUN bun install --frozen-lockfile COPY ./frontend/public ./public COPY ./frontend/src ./src diff --git a/Dockerfile.distroless b/Dockerfile.distroless index 068f7550..786a1ef9 100644 --- a/Dockerfile.distroless +++ b/Dockerfile.distroless @@ -6,7 +6,7 @@ WORKDIR /frontend COPY ./frontend/package.json ./ COPY ./frontend/bun.lock ./ -RUN bun install +RUN bun install --frozen-lockfile COPY ./frontend/public ./public COPY ./frontend/src ./src From 76f201444416c1a85a3d34d77a3f656ae574e0f9 Mon Sep 17 00:00:00 2001 From: CzBiX Date: Wed, 8 Oct 2025 23:58:22 +0800 Subject: [PATCH 070/127] feat: add http cache for static files (#395) * feat: add http cache for static files fix #392 * minor typo fix --- internal/middleware/ui_middleware.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/internal/middleware/ui_middleware.go b/internal/middleware/ui_middleware.go index ff028a16..adcb784a 100644 --- a/internal/middleware/ui_middleware.go +++ b/internal/middleware/ui_middleware.go @@ -1,10 +1,12 @@ package middleware import ( + "fmt" "io/fs" "net/http" "os" "strings" + "time" "tinyauth/internal/assets" "github.com/gin-gonic/gin" @@ -27,14 +29,16 @@ func (m *UIMiddleware) Init() error { } m.uiFs = ui - m.uiFileServer = http.FileServer(http.FS(ui)) + m.uiFileServer = http.FileServerFS(ui) return nil } func (m *UIMiddleware) Middleware() gin.HandlerFunc { return func(c *gin.Context) { - switch strings.Split(c.Request.URL.Path, "/")[1] { + path := strings.TrimPrefix(c.Request.URL.Path, "/") + + switch strings.SplitN(path, "/", 2)[0] { case "api": c.Next() return @@ -42,12 +46,19 @@ func (m *UIMiddleware) Middleware() gin.HandlerFunc { c.Next() return default: - _, err := fs.Stat(m.uiFs, strings.TrimPrefix(c.Request.URL.Path, "/")) + _, err := fs.Stat(m.uiFs, path) + + // Enough for one authentication flow + maxAge := 15 * time.Minute if os.IsNotExist(err) { c.Request.URL.Path = "/" + } else if strings.HasPrefix(path, "assets/") { + // assets are named with a hash and can be cached for a long time + maxAge = 30 * 24 * time.Hour } + c.Writer.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", int(maxAge.Seconds()))) m.uiFileServer.ServeHTTP(c.Writer, c.Request) c.Abort() return From ae8347fd28fec6c6fac285f0278eebca6a7c16ed Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 8 Oct 2025 23:11:18 +0300 Subject: [PATCH 071/127] fix: use docker meta flavors for handling latest, prefix and suffix --- .github/workflows/nightly.yml | 4 ++++ .github/workflows/release.yml | 19 +++++++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index bb3c859b..ab4a9900 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -396,6 +396,8 @@ jobs: uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository_owner }}/tinyauth + flavor: | + latest=false tags: | type=raw,nightly @@ -433,6 +435,8 @@ jobs: uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository_owner }}/tinyauth + flavor: | + latest=false tags: | type=raw,nightly-distroless diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9d09e04a..93972a41 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -359,10 +359,13 @@ jobs: uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository_owner }}/tinyauth + flavor: | + latest=true + prefix=v,onlatest=false tags: | - type=semver,pattern={{version}},prefix=v - type=semver,pattern={{major}},prefix=v - type=semver,pattern={{major}}.{{minor}},prefix=v + type=semver,pattern={{version}} + type=semver,pattern={{major}} + type=semver,pattern={{major}}.{{minor}} - name: Create manifest list and push working-directory: ${{ runner.temp }}/digests @@ -398,10 +401,14 @@ jobs: uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository_owner }}/tinyauth + flavor: | + latest=false + prefix=v,onlatest=false + suffix=-distroless,onlatest=false tags: | - type=semver,pattern={{version}},prefix=v,suffix=-distroless - type=semver,pattern={{major}},prefix=v,suffix=-distroless - type=semver,pattern={{major}}.{{minor}},prefix=v,suffix=-distroless + type=semver,pattern={{version}} + type=semver,pattern={{major}} + type=semver,pattern={{major}}.{{minor}} - name: Create manifest list and push working-directory: ${{ runner.temp }}/digests From 05d4dbd68e2547657c0e93d7d2124e273534e06e Mon Sep 17 00:00:00 2001 From: Stavros Date: Fri, 10 Oct 2025 15:34:09 +0300 Subject: [PATCH 072/127] fix: ensure data directory exists on docker image --- Dockerfile | 2 ++ Dockerfile.distroless | 5 +++++ air.toml | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index fbd280b5..8e2797b6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,6 +47,8 @@ WORKDIR /tinyauth COPY --from=builder /tinyauth/tinyauth ./ +RUN mkdir -p /data + EXPOSE 3000 VOLUME ["/data"] diff --git a/Dockerfile.distroless b/Dockerfile.distroless index 786a1ef9..8572b7b0 100644 --- a/Dockerfile.distroless +++ b/Dockerfile.distroless @@ -38,6 +38,8 @@ COPY ./cmd ./cmd COPY ./internal ./internal COPY --from=frontend-builder /frontend/dist ./internal/assets/dist +RUN mkdir -p /data + RUN CGO_ENABLED=0 go build -ldflags "-s -w -X tinyauth/internal/config.Version=${VERSION} -X tinyauth/internal/config.CommitHash=${COMMIT_HASH} -X tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" # Runner @@ -47,6 +49,9 @@ WORKDIR /tinyauth COPY --from=builder /tinyauth/tinyauth ./ +# Since it's distroless, we need to copy the data directory from the builder stage +COPY --from=builder /tinyauth/data /data + EXPOSE 3000 VOLUME ["/data"] diff --git a/air.toml b/air.toml index f84163bc..1772c279 100644 --- a/air.toml +++ b/air.toml @@ -2,7 +2,7 @@ root = "/tinyauth" tmp_dir = "tmp" [build] -pre_cmd = ["mkdir -p internal/assets/dist", "echo 'backend running' > internal/assets/dist/index.html", "go install github.com/go-delve/delve/cmd/dlv@v1.25.0"] +pre_cmd = ["mkdir -p internal/assets/dist", "mkdir -p /data", "echo 'backend running' > internal/assets/dist/index.html", "go install github.com/go-delve/delve/cmd/dlv@v1.25.0"] cmd = "CGO_ENABLED=0 go build -gcflags=\"all=-N -l\" -o tmp/tinyauth ." bin = "/go/bin/dlv --listen :4000 --headless=true --api-version=2 --accept-multiclient --log=true exec tmp/tinyauth --continue --check-go-version=false" include_ext = ["go"] From 5a4855c12c05ef884a40e4a32f139cf27fa3f1b5 Mon Sep 17 00:00:00 2001 From: Stavros Date: Fri, 10 Oct 2025 15:45:04 +0300 Subject: [PATCH 073/127] refactor: move docker connection check to start up --- internal/service/docker_service.go | 36 ++++++++++++++++++------------ 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/internal/service/docker_service.go b/internal/service/docker_service.go index a23af1aa..2a4a9599 100644 --- a/internal/service/docker_service.go +++ b/internal/service/docker_service.go @@ -12,8 +12,9 @@ import ( ) type DockerService struct { - client *client.Client - context context.Context + client *client.Client + context context.Context + isConnected bool } func NewDockerService() *DockerService { @@ -31,10 +32,24 @@ func (docker *DockerService) Init() error { docker.client = client docker.context = ctx + + _, err = docker.client.Ping(docker.context) + + if err != nil { + log.Debug().Err(err).Msg("Docker not connected") + docker.isConnected = false + docker.client = nil + docker.context = nil + return nil + } + + docker.isConnected = true + log.Debug().Msg("Docker connected") + return nil } -func (docker *DockerService) GetContainers() ([]container.Summary, error) { +func (docker *DockerService) getContainers() ([]container.Summary, error) { containers, err := docker.client.ContainerList(docker.context, container.ListOptions{}) if err != nil { return nil, err @@ -42,7 +57,7 @@ func (docker *DockerService) GetContainers() ([]container.Summary, error) { return containers, nil } -func (docker *DockerService) InspectContainer(containerId string) (container.InspectResponse, error) { +func (docker *DockerService) inspectContainer(containerId string) (container.InspectResponse, error) { inspect, err := docker.client.ContainerInspect(docker.context, containerId) if err != nil { return container.InspectResponse{}, err @@ -50,26 +65,19 @@ func (docker *DockerService) InspectContainer(containerId string) (container.Ins return inspect, nil } -func (docker *DockerService) DockerConnected() bool { - _, err := docker.client.Ping(docker.context) - return err == nil -} - func (docker *DockerService) GetLabels(appDomain string) (config.App, error) { - isConnected := docker.DockerConnected() - - if !isConnected { + if !docker.isConnected { log.Debug().Msg("Docker not connected, returning empty labels") return config.App{}, nil } - containers, err := docker.GetContainers() + containers, err := docker.getContainers() if err != nil { return config.App{}, err } for _, ctr := range containers { - inspect, err := docker.InspectContainer(ctr.ID) + inspect, err := docker.inspectContainer(ctr.ID) if err != nil { return config.App{}, err } From 4047cea45106188d0aecd518fa646ae0709cc5d9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 15:52:45 +0300 Subject: [PATCH 074/127] docs: regenerate readme sponsors list (#402) Co-authored-by: GitHub --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 761ea98c..7ca9b9ac 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You ma A big thank you to the following people for providing me with more coffee: -User avatar: erwinkramer  User avatar: nicotsx  User avatar: SimpleHomelab  User avatar: jmadden91  User avatar: tribor  User avatar: eliasbenb  User avatar: afunworm   +User avatar: erwinkramer  User avatar: nicotsx  User avatar: SimpleHomelab  User avatar: jmadden91  User avatar: tribor  User avatar: eliasbenb  User avatar: afunworm  User avatar: chip-well  User avatar: Lancelot-Enguerrand   ## Acknowledgements From a1ec4a69cfc2358b7b0ad7b06d3b495e39124009 Mon Sep 17 00:00:00 2001 From: Stavros Date: Fri, 10 Oct 2025 16:28:52 +0300 Subject: [PATCH 075/127] fix: remove spaces before checking oauth name and username --- internal/controller/oauth_controller.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/controller/oauth_controller.go b/internal/controller/oauth_controller.go index bf50ff91..c0a7380b 100644 --- a/internal/controller/oauth_controller.go +++ b/internal/controller/oauth_controller.go @@ -162,7 +162,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { var name string - if user.Name != "" { + if strings.TrimSpace(user.Name) != "" { log.Debug().Msg("Using name from OAuth provider") name = user.Name } else { @@ -172,7 +172,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { var username string - if user.PreferredUsername != "" { + if strings.TrimSpace(user.PreferredUsername) != "" { log.Debug().Msg("Using preferred username from OAuth provider") username = user.PreferredUsername } else { From b940d681c3f1c4bd6d0eafa94c18075ac0f50a3d Mon Sep 17 00:00:00 2001 From: Stavros Date: Fri, 10 Oct 2025 16:42:19 +0300 Subject: [PATCH 076/127] feat: use recovery gin middleware in engine --- internal/bootstrap/app_bootstrap.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index d3dbfc2d..a757a2e5 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -173,6 +173,7 @@ func (app *BootstrapApp) Setup() error { // Create engine engine := gin.New() + engine.Use(gin.Recovery()) if len(app.config.TrustedProxies) > 0 { err := engine.SetTrustedProxies(strings.Split(app.config.TrustedProxies, ",")) From fc7e395e66b3a8d43f8af83e5929dc2b91d92828 Mon Sep 17 00:00:00 2001 From: Stavros Date: Fri, 10 Oct 2025 17:16:22 +0300 Subject: [PATCH 077/127] feat: sort configured providers based on name length --- internal/bootstrap/app_bootstrap.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index a757a2e5..606dd0b9 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -7,6 +7,7 @@ import ( "net/http" "net/url" "os" + "sort" "strings" "time" "tinyauth/internal/config" @@ -157,6 +158,10 @@ func (app *BootstrapApp) Setup() error { }) } + sort.Slice(configuredProviders, func(i, j int) bool { + return configuredProviders[i].Name < configuredProviders[j].Name + }) + if authService.UserAuthConfigured() || ldapService != nil { configuredProviders = append(configuredProviders, controller.Provider{ Name: "Username", From ed20d2cf51a4f24b227ebe631243c98f1d1770db Mon Sep 17 00:00:00 2001 From: Stavros Date: Sat, 11 Oct 2025 15:18:37 +0300 Subject: [PATCH 078/127] fix: use correct path for data directory in distroless --- Dockerfile.distroless | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.distroless b/Dockerfile.distroless index 8572b7b0..91f5bc9a 100644 --- a/Dockerfile.distroless +++ b/Dockerfile.distroless @@ -38,7 +38,7 @@ COPY ./cmd ./cmd COPY ./internal ./internal COPY --from=frontend-builder /frontend/dist ./internal/assets/dist -RUN mkdir -p /data +RUN mkdir -p data RUN CGO_ENABLED=0 go build -ldflags "-s -w -X tinyauth/internal/config.Version=${VERSION} -X tinyauth/internal/config.CommitHash=${COMMIT_HASH} -X tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" From 9b76a84ee2007dfb36a3e1d3e80eb47272da917b Mon Sep 17 00:00:00 2001 From: Stavros Date: Sat, 11 Oct 2025 15:27:01 +0300 Subject: [PATCH 079/127] feat: add trace logging --- cmd/root.go | 4 ++++ internal/controller/proxy_controller.go | 4 ++++ internal/service/auth_service.go | 1 + internal/service/docker_service.go | 4 ++-- internal/service/generic_oauth_service.go | 3 +++ 5 files changed, 14 insertions(+), 2 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index a0ce20d4..99b6a45f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -112,6 +112,10 @@ func (c *rootCmd) run(cmd *cobra.Command, args []string) { log.Logger = log.Level(zerolog.Level(utils.GetLogLevel(conf.LogLevel))) log.Info().Str("version", strings.TrimSpace(config.Version)).Msg("Starting Tinyauth") + if log.Logger.GetLevel() == zerolog.TraceLevel { + log.Warn().Msg("Log level set to trace, this will log sensitive information!") + } + app := bootstrap.NewBootstrapApp(conf) err = app.Setup() diff --git a/internal/controller/proxy_controller.go b/internal/controller/proxy_controller.go index dbf13b92..8ded9dcd 100644 --- a/internal/controller/proxy_controller.go +++ b/internal/controller/proxy_controller.go @@ -84,6 +84,8 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { return } + log.Trace().Interface("labels", labels).Msg("Labels for resource") + clientIP := c.ClientIP() if controller.auth.IsBypassedIP(labels.IP, clientIP) { @@ -150,6 +152,8 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { userContext = context } + log.Trace().Interface("context", userContext).Msg("User context from request") + if userContext.Provider == "basic" && userContext.TotpEnabled { log.Debug().Msg("User has TOTP enabled, denying basic auth access") userContext.IsLoggedIn = false diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index 4cd66b8a..d9f792b0 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -318,6 +318,7 @@ func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserConte for userGroup := range strings.SplitSeq(context.OAuthGroups, ",") { if utils.CheckFilter(requiredGroups, strings.TrimSpace(userGroup)) { + log.Trace().Str("group", userGroup).Str("required", requiredGroups).Msg("User group matched") return true } } diff --git a/internal/service/docker_service.go b/internal/service/docker_service.go index 2a4a9599..51397574 100644 --- a/internal/service/docker_service.go +++ b/internal/service/docker_service.go @@ -89,12 +89,12 @@ func (docker *DockerService) GetLabels(appDomain string) (config.App, error) { for appName, appLabels := range labels.Apps { if appLabels.Config.Domain == appDomain { - log.Debug().Str("id", inspect.ID).Msg("Found matching container by domain") + log.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by domain") return appLabels, nil } if strings.TrimPrefix(inspect.Name, "/") == appName { - log.Debug().Str("id", inspect.ID).Msg("Found matching container by app name") + log.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by app name") return appLabels, nil } } diff --git a/internal/service/generic_oauth_service.go b/internal/service/generic_oauth_service.go index aae89c46..053944ad 100644 --- a/internal/service/generic_oauth_service.go +++ b/internal/service/generic_oauth_service.go @@ -12,6 +12,7 @@ import ( "time" "tinyauth/internal/config" + "github.com/rs/zerolog/log" "golang.org/x/oauth2" ) @@ -110,6 +111,8 @@ func (generic *GenericOAuthService) Userinfo() (config.Claims, error) { return user, err } + log.Trace().Str("body", string(body)).Msg("Userinfo response body") + err = json.Unmarshal(body, &user) if err != nil { return user, err From dc67be2ba0ec0341974f8d9323dc3adc8bc6f94e Mon Sep 17 00:00:00 2001 From: Stavros Date: Sat, 11 Oct 2025 23:18:20 +0300 Subject: [PATCH 080/127] fix: remove html suffix from docs link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7ca9b9ac..934fc761 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Tinyauth is a simple authentication middleware that adds a simple login screen o ## Getting Started -You can easily get started with Tinyauth by following the guide in the [documentation](https://tinyauth.app/docs/getting-started.html). There is also an available [docker compose](./docker-compose.example.yml) file that has Traefik, Whoami and Tinyauth to demonstrate its capabilities. +You can easily get started with Tinyauth by following the guide in the [documentation](https://tinyauth.app/docs/getting-started). There is also an available [docker compose](./docker-compose.example.yml) file that has Traefik, Whoami and Tinyauth to demonstrate its capabilities. ## Demo From 1b87ed9b99260ecb039f4c52527ba5f53e086f84 Mon Sep 17 00:00:00 2001 From: Stavros Date: Sat, 11 Oct 2025 16:02:18 +0300 Subject: [PATCH 081/127] feat: add config dumps to trace log level --- internal/bootstrap/app_bootstrap.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index 606dd0b9..2cb7e979 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -75,6 +75,15 @@ func (app *BootstrapApp) Setup() error { csrfCookieName := fmt.Sprintf("%s-%s", config.CSRFCookieName, cookieId) redirectCookieName := fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId) + // Dumps + log.Trace().Interface("config", app.config).Msg("Config dump") + log.Trace().Interface("users", users).Msg("Users dump") + log.Trace().Interface("oauthProviders", oauthProviders).Msg("OAuth providers dump") + log.Trace().Str("cookieDomain", cookieDomain).Msg("Cookie domain") + log.Trace().Str("sessionCookieName", sessionCookieName).Msg("Session cookie name") + log.Trace().Str("csrfCookieName", csrfCookieName).Msg("CSRF cookie name") + log.Trace().Str("redirectCookieName", redirectCookieName).Msg("Redirect cookie name") + // Create configs authConfig := service.AuthServiceConfig{ Users: users, From 64222b6d152a4533b205c64926ed21e9d3692cc3 Mon Sep 17 00:00:00 2001 From: Stavros Date: Sun, 12 Oct 2025 11:29:23 +0300 Subject: [PATCH 082/127] fix: don't use container name in label discovery --- internal/service/docker_service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/docker_service.go b/internal/service/docker_service.go index 51397574..b0f977d3 100644 --- a/internal/service/docker_service.go +++ b/internal/service/docker_service.go @@ -93,7 +93,7 @@ func (docker *DockerService) GetLabels(appDomain string) (config.App, error) { return appLabels, nil } - if strings.TrimPrefix(inspect.Name, "/") == appName { + if strings.SplitN(appDomain, ".", 2)[0] == appName { log.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by app name") return appLabels, nil } From 0996711f08c755af8f616eaa39024d0f557a9363 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 12 Oct 2025 11:51:04 +0300 Subject: [PATCH 083/127] chore(deps): bump the minor-patch group with 3 updates (#396) Bumps the minor-patch group with 3 updates: [golang.org/x/crypto](https://github.com/golang/crypto), [github.com/docker/docker](https://github.com/docker/docker) and [golang.org/x/oauth2](https://github.com/golang/oauth2). Updates `golang.org/x/crypto` from 0.42.0 to 0.43.0 - [Commits](https://github.com/golang/crypto/compare/v0.42.0...v0.43.0) Updates `github.com/docker/docker` from 28.5.0+incompatible to 28.5.1+incompatible - [Release notes](https://github.com/docker/docker/releases) - [Commits](https://github.com/docker/docker/compare/v28.5.0...v28.5.1) Updates `golang.org/x/oauth2` from 0.31.0 to 0.32.0 - [Commits](https://github.com/golang/oauth2/compare/v0.31.0...v0.32.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.43.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: github.com/docker/docker dependency-version: 28.5.1+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: golang.org/x/oauth2 dependency-version: 0.32.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 18 +++++++++--------- go.sum | 36 ++++++++++++++++++------------------ 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/go.mod b/go.mod index 17216945..8dedbe03 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/spf13/viper v1.21.0 github.com/traefik/paerser v0.2.2 github.com/weppos/publicsuffix-go v0.50.0 - golang.org/x/crypto v0.42.0 + golang.org/x/crypto v0.43.0 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b gorm.io/gorm v1.31.0 gotest.tools/v3 v3.5.2 @@ -53,9 +53,9 @@ require ( go.opentelemetry.io/otel/sdk v1.34.0 // indirect go.uber.org/mock v0.5.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/mod v0.27.0 // indirect - golang.org/x/term v0.35.0 // indirect - golang.org/x/tools v0.36.0 // indirect + golang.org/x/mod v0.28.0 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/tools v0.37.0 // indirect modernc.org/libc v1.66.3 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect @@ -80,7 +80,7 @@ require ( github.com/charmbracelet/x/term v0.2.1 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/docker v28.5.0+incompatible + github.com/docker/docker v28.5.1+incompatible 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 @@ -130,10 +130,10 @@ require ( go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect golang.org/x/arch v0.20.0 // indirect - golang.org/x/net v0.44.0 // indirect - golang.org/x/oauth2 v0.31.0 + golang.org/x/net v0.45.0 // indirect + golang.org/x/oauth2 v0.32.0 golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/text v0.29.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect google.golang.org/protobuf v1.36.9 // indirect ) diff --git a/go.sum b/go.sum index d6690a6b..e18a52b2 100644 --- a/go.sum +++ b/go.sum @@ -72,8 +72,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/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 v28.5.0+incompatible h1:ZdSQoRUE9XxhFI/B8YLvhnEFMmYN9Pp8Egd2qcaFk1E= -github.com/docker/docker v28.5.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= +github.com/docker/docker v28.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= @@ -304,32 +304,32 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= -golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= -golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= -golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= -golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= +golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= From 8ee331a564015f4f748615e90a8524f3733fc182 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 12 Oct 2025 11:51:44 +0300 Subject: [PATCH 084/127] chore(deps-dev): bump eslint-plugin-react-hooks in /frontend (#398) Bumps [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) from 6.1.1 to 7.0.0. - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/HEAD/packages/eslint-plugin-react-hooks) --- updated-dependencies: - dependency-name: eslint-plugin-react-hooks dependency-version: 7.0.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/bun.lock | 10 ++++++---- frontend/package.json | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/bun.lock b/frontend/bun.lock index d74b1d81..cc3b00aa 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -39,7 +39,7 @@ "@types/react-dom": "^19.2.1", "@vitejs/plugin-react": "^5.0.4", "eslint": "^9.37.0", - "eslint-plugin-react-hooks": "^6.1.1", + "eslint-plugin-react-hooks": "^7.0.0", "eslint-plugin-react-refresh": "^0.4.23", "globals": "^16.4.0", "prettier": "3.6.2", @@ -493,7 +493,7 @@ "eslint": ["eslint@9.37.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.4.0", "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.37.0", "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig=="], - "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@6.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "zod": "^3.22.4 || ^4.0.0", "zod-validation-error": "^3.0.3 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-St9EKZzOAQF704nt2oJvAKZHjhrpg25ClQoaAlHmPZuajFldVLqRDW4VBNAS01NzeiQF0m0qhG1ZA807K6aVaQ=="], + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.0", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.22.4 || ^4.0.0", "zod-validation-error": "^3.0.3 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-fNXaOwvKwq2+pXiRpXc825Vd63+KM4DLL40Rtlycb8m7fYpp6efrTp1sa6ZbP/Ap58K2bEKFXRmhURE+CJAQWw=="], "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.23", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-G4j+rv0NmbIR45kni5xJOrYvCtyD3/7LjpVH8MPPcudXDcNu8gv+4ATTDXTtbRR8rTCM5HxECvCSsRmxKnWDsA=="], @@ -575,6 +575,10 @@ "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], + + "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], + "html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="], "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], @@ -995,8 +999,6 @@ "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="], - "eslint-plugin-react-hooks/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="], - "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "hast-util-to-jsx-runtime/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], diff --git a/frontend/package.json b/frontend/package.json index e0a270f1..cd970ff3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -45,7 +45,7 @@ "@types/react-dom": "^19.2.1", "@vitejs/plugin-react": "^5.0.4", "eslint": "^9.37.0", - "eslint-plugin-react-hooks": "^6.1.1", + "eslint-plugin-react-hooks": "^7.0.0", "eslint-plugin-react-refresh": "^0.4.23", "globals": "^16.4.0", "prettier": "3.6.2", From 81136eeb420d8c50a0b01a5072a020da0236b079 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 12 Oct 2025 11:52:12 +0300 Subject: [PATCH 085/127] chore(deps): bump the minor-patch group across 1 directory with 2 updates (#400) Bumps the minor-patch group with 2 updates in the /frontend directory: [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node). Updates `react-router` from 7.9.3 to 7.9.4 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router@7.9.4/packages/react-router) Updates `@types/node` from 24.7.0 to 24.7.1 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: react-router dependency-version: 7.9.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: "@types/node" dependency-version: 24.7.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/bun.lock | 8 ++++---- frontend/package.json | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/bun.lock b/frontend/bun.lock index cc3b00aa..a079f838 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -25,7 +25,7 @@ "react-hook-form": "^7.64.0", "react-i18next": "^16.0.0", "react-markdown": "^10.1.0", - "react-router": "^7.9.3", + "react-router": "^7.9.4", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.14", @@ -34,7 +34,7 @@ "devDependencies": { "@eslint/js": "^9.37.0", "@tanstack/eslint-plugin-query": "^5.91.0", - "@types/node": "^24.7.0", + "@types/node": "^24.7.1", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.1", "@vitejs/plugin-react": "^5.0.4", @@ -355,7 +355,7 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@24.7.0", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw=="], + "@types/node": ["@types/node@24.7.1", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-CmyhGZanP88uuC5GpWU9q+fI61j2SkhO3UGMUdfYRE6Bcy0ccyzn1Rqj9YAB/ZY4kOXmNf0ocah5GtphmLMP6Q=="], "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], @@ -803,7 +803,7 @@ "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], - "react-router": ["react-router@7.9.3", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-4o2iWCFIwhI/eYAIL43+cjORXYn/aRQPgtFRRZb3VzoyQ5Uej0Bmqj7437L97N9NJW4wnicSwLOLS+yCXfAPgg=="], + "react-router": ["react-router@7.9.4", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA=="], "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], diff --git a/frontend/package.json b/frontend/package.json index cd970ff3..3a6336e8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,7 +31,7 @@ "react-hook-form": "^7.64.0", "react-i18next": "^16.0.0", "react-markdown": "^10.1.0", - "react-router": "^7.9.3", + "react-router": "^7.9.4", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.14", @@ -40,7 +40,7 @@ "devDependencies": { "@eslint/js": "^9.37.0", "@tanstack/eslint-plugin-query": "^5.91.0", - "@types/node": "^24.7.0", + "@types/node": "^24.7.1", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.1", "@vitejs/plugin-react": "^5.0.4", From a9c1bf8865c4609cfaee1b52ca5c5778bb335c76 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 12 Oct 2025 11:53:14 +0300 Subject: [PATCH 086/127] chore(deps): bump github.com/quic-go/quic-go from 0.54.0 to 0.54.1 (#403) Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.54.0 to 0.54.1. - [Release notes](https://github.com/quic-go/quic-go/releases) - [Commits](https://github.com/quic-go/quic-go/compare/v0.54.0...v0.54.1) --- updated-dependencies: - dependency-name: github.com/quic-go/quic-go dependency-version: 0.54.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8dedbe03..71e8cdf9 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ require ( github.com/moby/term v0.5.2 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/quic-go/qpack v0.5.1 // indirect - github.com/quic-go/quic-go v0.54.0 // indirect + github.com/quic-go/quic-go v0.54.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect diff --git a/go.sum b/go.sum index e18a52b2..1d9136e7 100644 --- a/go.sum +++ b/go.sum @@ -229,8 +229,8 @@ github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= -github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/quic-go/quic-go v0.54.1 h1:4ZAWm0AhCb6+hE+l5Q1NAL0iRn/ZrMwqHRGQiFwj2eg= +github.com/quic-go/quic-go v0.54.1/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= From f628d1f0b3461890b699781557ec4aa5d88caef8 Mon Sep 17 00:00:00 2001 From: Scott McKendry Date: Mon, 13 Oct 2025 21:55:43 +1300 Subject: [PATCH 087/127] fix(redirect): allow root cookie domain host redirects (#409) Previously IsRedirectSafe rejected redirects to the exact cookie domain when AppURL had multiple subdomain levels, because it stripped the first label twice. --- internal/utils/app_utils.go | 12 +++++------ internal/utils/app_utils_test.go | 37 +++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/internal/utils/app_utils.go b/internal/utils/app_utils.go index de0cff63..7d143ac4 100644 --- a/internal/utils/app_utils.go +++ b/internal/utils/app_utils.go @@ -100,17 +100,17 @@ func IsRedirectSafe(redirectURL string, domain string) bool { return false } - cookieDomain, err := GetCookieDomain(redirectURL) - - if err != nil { - return false + host := parsedURL.Hostname() + if host == domain { + return true } - if cookieDomain != domain { + cookieDomain, err := GetCookieDomain(redirectURL) + if err != nil { return false } - return true + return cookieDomain == domain } func GetLogLevel(level string) zerolog.Level { diff --git a/internal/utils/app_utils_test.go b/internal/utils/app_utils_test.go index d8abcab4..e6cdaebe 100644 --- a/internal/utils/app_utils_test.go +++ b/internal/utils/app_utils_test.go @@ -164,7 +164,7 @@ func TestIsRedirectSafe(t *testing.T) { // Case with no subdomain redirectURL := "http://example.com/welcome" result := utils.IsRedirectSafe(redirectURL, domain) - assert.Equal(t, false, result) + assert.Equal(t, true, result) // Case with different domain redirectURL = "http://malicious.com/phishing" @@ -202,6 +202,41 @@ func TestIsRedirectSafe(t *testing.T) { assert.Equal(t, false, result) } +func TestIsRedirectSafeMultiLevel(t *testing.T) { + // Setup + cookieDomain := "tinyauth.example.com" + + // Case with 3rd level domain + redirectURL := "http://tinyauth.example.com/welcome" + result := utils.IsRedirectSafe(redirectURL, cookieDomain) + assert.Equal(t, true, result) + + // Case with root domain + redirectURL = "http://example.com/unsafe" + result = utils.IsRedirectSafe(redirectURL, cookieDomain) + assert.Equal(t, false, result) + + // Case with 4th level domain + redirectURL = "http://auth.tinyauth.example.com/post-login" + result = utils.IsRedirectSafe(redirectURL, cookieDomain) + assert.Equal(t, true, result) + + // Case with 5th level domain (should be unsafe) + redirectURL = "http://x.auth.tinyauth.example.com/deep" + result = utils.IsRedirectSafe(redirectURL, cookieDomain) + assert.Equal(t, false, result) + + // Case with different subdomain + redirectURL = "http://auth.tinyauth.example.net/attack" + result = utils.IsRedirectSafe(redirectURL, cookieDomain) + assert.Equal(t, false, result) + + // Case with malformed URL + redirectURL = "http://[::1]:namedport" + result = utils.IsRedirectSafe(redirectURL, cookieDomain) + assert.Equal(t, false, result) +} + func TestGetOAuthProvidersConfig(t *testing.T) { env := []string{"PROVIDERS_CLIENT1_CLIENT_ID=client1-id", "PROVIDERS_CLIENT1_CLIENT_SECRET=client1-secret"} args := []string{"/tinyauth/tinyauth", "--providers-client2-client-id=client2-id", "--providers-client2-client-secret=client2-secret"} From 473109b36aafd54756d7917220a41122b8182f85 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 19:14:11 +0300 Subject: [PATCH 088/127] chore(deps): bump oven/bun from 1.2.23-alpine to 1.3.0-alpine (#410) Bumps oven/bun from 1.2.23-alpine to 1.3.0-alpine. --- updated-dependencies: - dependency-name: oven/bun dependency-version: 1.3.0-alpine dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Dockerfile | 2 +- Dockerfile.distroless | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8e2797b6..caeff384 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Site builder -FROM oven/bun:1.2.23-alpine AS frontend-builder +FROM oven/bun:1.3.0-alpine AS frontend-builder WORKDIR /frontend diff --git a/Dockerfile.distroless b/Dockerfile.distroless index 91f5bc9a..377fa618 100644 --- a/Dockerfile.distroless +++ b/Dockerfile.distroless @@ -1,5 +1,5 @@ # Site builder -FROM oven/bun:1.2.23-alpine AS frontend-builder +FROM oven/bun:1.3.0-alpine AS frontend-builder WORKDIR /frontend From 2ea921f3cad2a46c2ec02873a9a5b27124a6dbdc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 19:14:30 +0300 Subject: [PATCH 089/127] chore(deps): bump the minor-patch group in /frontend with 3 updates (#411) Bumps the minor-patch group in /frontend with 3 updates: [i18next](https://github.com/i18next/i18next), [react-hook-form](https://github.com/react-hook-form/react-hook-form) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node). Updates `i18next` from 25.5.3 to 25.6.0 - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/i18next/compare/v25.5.3...v25.6.0) Updates `react-hook-form` from 7.64.0 to 7.65.0 - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.64.0...v7.65.0) Updates `@types/node` from 24.7.1 to 24.7.2 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: i18next dependency-version: 25.6.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: react-hook-form dependency-version: 7.65.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@types/node" dependency-version: 24.7.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/bun.lock | 12 ++++++------ frontend/package.json | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/frontend/bun.lock b/frontend/bun.lock index a079f838..5c6d3057 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -14,7 +14,7 @@ "axios": "^1.12.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "i18next": "^25.5.3", + "i18next": "^25.6.0", "i18next-browser-languagedetector": "^8.2.0", "i18next-resources-to-backend": "^1.2.1", "input-otp": "^1.4.2", @@ -22,7 +22,7 @@ "next-themes": "^0.4.6", "react": "^19.2.0", "react-dom": "^19.2.0", - "react-hook-form": "^7.64.0", + "react-hook-form": "^7.65.0", "react-i18next": "^16.0.0", "react-markdown": "^10.1.0", "react-router": "^7.9.4", @@ -34,7 +34,7 @@ "devDependencies": { "@eslint/js": "^9.37.0", "@tanstack/eslint-plugin-query": "^5.91.0", - "@types/node": "^24.7.1", + "@types/node": "^24.7.2", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.1", "@vitejs/plugin-react": "^5.0.4", @@ -355,7 +355,7 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@24.7.1", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-CmyhGZanP88uuC5GpWU9q+fI61j2SkhO3UGMUdfYRE6Bcy0ccyzn1Rqj9YAB/ZY4kOXmNf0ocah5GtphmLMP6Q=="], + "@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="], "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], @@ -583,7 +583,7 @@ "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], - "i18next": ["i18next@25.5.3", "", { "dependencies": { "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-joFqorDeQ6YpIXni944upwnuHBf5IoPMuqAchGVeQLdWC2JOjxgM9V8UGLhNIIH/Q8QleRxIi0BSRQehSrDLcg=="], + "i18next": ["i18next@25.6.0", "", { "dependencies": { "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-tTn8fLrwBYtnclpL5aPXK/tAYBLWVvoHM1zdfXoRNLcI+RvtMsoZRV98ePlaW3khHYKuNh/Q65W/+NVFUeIwVw=="], "i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.0", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g=="], @@ -791,7 +791,7 @@ "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], - "react-hook-form": ["react-hook-form@7.64.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-fnN+vvTiMLnRqKNTVhDysdrUay0kUUAymQnFIznmgDvapjveUWOOPqMNzPg+A+0yf9DuE2h6xzBjN1s+Qx8wcg=="], + "react-hook-form": ["react-hook-form@7.65.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw=="], "react-i18next": ["react-i18next@16.0.0", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 25.5.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-JQ+dFfLnFSKJQt7W01lJHWRC0SX7eDPobI+MSTJ3/gP39xH2g33AuTE7iddAfXYHamJdAeMGM0VFboPaD3G68Q=="], diff --git a/frontend/package.json b/frontend/package.json index 3a6336e8..74671191 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,7 +20,7 @@ "axios": "^1.12.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "i18next": "^25.5.3", + "i18next": "^25.6.0", "i18next-browser-languagedetector": "^8.2.0", "i18next-resources-to-backend": "^1.2.1", "input-otp": "^1.4.2", @@ -28,7 +28,7 @@ "next-themes": "^0.4.6", "react": "^19.2.0", "react-dom": "^19.2.0", - "react-hook-form": "^7.64.0", + "react-hook-form": "^7.65.0", "react-i18next": "^16.0.0", "react-markdown": "^10.1.0", "react-router": "^7.9.4", @@ -40,7 +40,7 @@ "devDependencies": { "@eslint/js": "^9.37.0", "@tanstack/eslint-plugin-query": "^5.91.0", - "@types/node": "^24.7.1", + "@types/node": "^24.7.2", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.1", "@vitejs/plugin-react": "^5.0.4", From 57b7b668130d3fabe07e3a82850d0d45281b1bce Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 14 Oct 2025 15:45:50 +0300 Subject: [PATCH 090/127] fix: let docker meta decide if the version is latest or not --- .github/workflows/release.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 93972a41..0cf3dde7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -360,7 +360,6 @@ jobs: with: images: ghcr.io/${{ github.repository_owner }}/tinyauth flavor: | - latest=true prefix=v,onlatest=false tags: | type=semver,pattern={{version}} @@ -403,8 +402,8 @@ jobs: images: ghcr.io/${{ github.repository_owner }}/tinyauth flavor: | latest=false - prefix=v,onlatest=false - suffix=-distroless,onlatest=false + prefix=v + suffix=-distroless tags: | type=semver,pattern={{version}} type=semver,pattern={{major}} From 82350594c16ac91ad1b00357768df82ddb09c833 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 19:04:58 +0300 Subject: [PATCH 091/127] chore(deps): bump github.com/charmbracelet/huh in the minor-patch group (#414) Bumps the minor-patch group with 1 update: [github.com/charmbracelet/huh](https://github.com/charmbracelet/huh). Updates `github.com/charmbracelet/huh` from 0.7.0 to 0.8.0 - [Release notes](https://github.com/charmbracelet/huh/releases) - [Commits](https://github.com/charmbracelet/huh/compare/v0.7.0...v0.8.0) --- updated-dependencies: - dependency-name: github.com/charmbracelet/huh dependency-version: 0.8.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 71e8cdf9..12b29b91 100644 --- a/go.mod +++ b/go.mod @@ -71,11 +71,11 @@ require ( github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/catppuccin/go v0.3.0 // indirect - github.com/charmbracelet/bubbles v0.21.0 // indirect - github.com/charmbracelet/bubbletea v1.3.4 // indirect - github.com/charmbracelet/huh v0.7.0 + github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect + github.com/charmbracelet/bubbletea v1.3.6 // indirect + github.com/charmbracelet/huh v0.8.0 github.com/charmbracelet/lipgloss v1.1.0 // indirect - github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/ansi v0.9.3 // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/cloudwego/base64x v0.1.6 // indirect diff --git a/go.sum b/go.sum index 1d9136e7..be59848a 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z 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/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= -github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= +github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4= github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= @@ -27,18 +27,18 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= -github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= -github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= -github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= -github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw= +github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= +github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= -github.com/charmbracelet/huh v0.7.0 h1:W8S1uyGETgj9Tuda3/JdVkc3x7DBLZYPZc4c+/rnRdc= -github.com/charmbracelet/huh v0.7.0/go.mod h1:UGC3DZHlgOKHvHC07a5vHag41zzhpPFj34U92sOmyuk= +github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY= +github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= -github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= -github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= +github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= From 97639ae90303de852df9e667ae496085256d0db6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 19:05:21 +0300 Subject: [PATCH 092/127] chore(deps): bump the minor-patch group across 1 directory with 5 updates (#415) Bumps the minor-patch group with 5 updates in the /frontend directory: | Package | From | To | | --- | --- | --- | | [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.90.2` | `5.90.3` | | [react-i18next](https://github.com/i18next/react-i18next) | `16.0.0` | `16.0.1` | | [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `19.2.1` | `19.2.2` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.46.0` | `8.46.1` | | [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.1.9` | `7.1.10` | Updates `@tanstack/react-query` from 5.90.2 to 5.90.3 - [Release notes](https://github.com/TanStack/query/releases) - [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md) - [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.90.3/packages/react-query) Updates `react-i18next` from 16.0.0 to 16.0.1 - [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/react-i18next/compare/v16.0.0...v16.0.1) Updates `@types/react-dom` from 19.2.1 to 19.2.2 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom) Updates `typescript-eslint` from 8.46.0 to 8.46.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.46.1/packages/typescript-eslint) Updates `vite` from 7.1.9 to 7.1.10 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v7.1.10/packages/vite) --- updated-dependencies: - dependency-name: "@tanstack/react-query" dependency-version: 5.90.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: react-i18next dependency-version: 16.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: "@types/react-dom" dependency-version: 19.2.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: typescript-eslint dependency-version: 8.46.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: vite dependency-version: 7.1.10 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/bun.lock | 66 +++++++++++++++++++++---------------------- frontend/package.json | 10 +++---- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/frontend/bun.lock b/frontend/bun.lock index 5c6d3057..03217b35 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -10,7 +10,7 @@ "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@tailwindcss/vite": "^4.1.14", - "@tanstack/react-query": "^5.90.2", + "@tanstack/react-query": "^5.90.3", "axios": "^1.12.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -23,7 +23,7 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "react-hook-form": "^7.65.0", - "react-i18next": "^16.0.0", + "react-i18next": "^16.0.1", "react-markdown": "^10.1.0", "react-router": "^7.9.4", "sonner": "^2.0.7", @@ -36,7 +36,7 @@ "@tanstack/eslint-plugin-query": "^5.91.0", "@types/node": "^24.7.2", "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.1", + "@types/react-dom": "^19.2.2", "@vitejs/plugin-react": "^5.0.4", "eslint": "^9.37.0", "eslint-plugin-react-hooks": "^7.0.0", @@ -45,8 +45,8 @@ "prettier": "3.6.2", "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", - "typescript-eslint": "^8.46.0", - "vite": "^7.1.9", + "typescript-eslint": "^8.46.1", + "vite": "^7.1.10", }, }, }, @@ -329,9 +329,9 @@ "@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.91.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.44.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-Kn6yWyRe3dIPf7NqyDMhcsTBz2Oh8jPSOpBdlnLQhGBJ6iTMBFYA4B1UreGJ/WdfzQskSMh5imcyWF+wqa/Q5g=="], - "@tanstack/query-core": ["@tanstack/query-core@5.90.2", "", {}, "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ=="], + "@tanstack/query-core": ["@tanstack/query-core@5.90.3", "", {}, "sha512-HtPOnCwmx4dd35PfXU8jjkhwYrsHfuqgC8RCJIwWglmhIUIlzPP0ZcEkDAc+UtAWCiLm7T8rxeEfHZlz3hYMCA=="], - "@tanstack/react-query": ["@tanstack/react-query@5.90.2", "", { "dependencies": { "@tanstack/query-core": "5.90.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw=="], + "@tanstack/react-query": ["@tanstack/react-query@5.90.3", "", { "dependencies": { "@tanstack/query-core": "5.90.3" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-i/LRL6DtuhG6bjGzavIMIVuKKPWx2AnEBIsBfuMm3YoHne0a20nWmsatOCBcVSaT0/8/5YFjNkebHAPLVUSi0Q=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -359,29 +359,29 @@ "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], - "@types/react-dom": ["@types/react-dom@19.2.1", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A=="], + "@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.46.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/type-utils": "8.46.0", "@typescript-eslint/utils": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.46.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.46.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/type-utils": "8.46.1", "@typescript-eslint/utils": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.46.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.46.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", "@typescript-eslint/typescript-estree": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.46.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.46.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.46.0", "@typescript-eslint/types": "^8.46.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.46.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.46.1", "@typescript-eslint/types": "^8.46.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg=="], "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.45.0", "", { "dependencies": { "@typescript-eslint/types": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0" } }, "sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.46.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.46.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.46.0", "", { "dependencies": { "@typescript-eslint/types": "8.46.0", "@typescript-eslint/typescript-estree": "8.46.0", "@typescript-eslint/utils": "8.46.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1", "@typescript-eslint/utils": "8.46.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw=="], "@typescript-eslint/types": ["@typescript-eslint/types@8.45.0", "", {}, "sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.46.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.46.0", "@typescript-eslint/tsconfig-utils": "8.46.0", "@typescript-eslint/types": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.46.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.46.1", "@typescript-eslint/tsconfig-utils": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg=="], "@typescript-eslint/utils": ["@typescript-eslint/utils@8.45.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/types": "8.45.0", "@typescript-eslint/typescript-estree": "8.45.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.46.0", "", { "dependencies": { "@typescript-eslint/types": "8.46.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], @@ -793,7 +793,7 @@ "react-hook-form": ["react-hook-form@7.65.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw=="], - "react-i18next": ["react-i18next@16.0.0", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 25.5.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-JQ+dFfLnFSKJQt7W01lJHWRC0SX7eDPobI+MSTJ3/gP39xH2g33AuTE7iddAfXYHamJdAeMGM0VFboPaD3G68Q=="], + "react-i18next": ["react-i18next@16.0.1", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 25.5.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-0S//bpYEkCPjzuVmxDf9Z6+Y+ArNvpAUk7eDL4qNCZXjDh6Z9j6MZ+NThU7kMCOsmYmDCun3GYEwkiOjjZo9Ug=="], "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], @@ -871,7 +871,7 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "typescript-eslint": ["typescript-eslint@8.46.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.46.0", "@typescript-eslint/parser": "8.46.0", "@typescript-eslint/typescript-estree": "8.46.0", "@typescript-eslint/utils": "8.46.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-6+ZrB6y2bT2DX3K+Qd9vn7OFOJR+xSLDj+Aw/N3zBwUt27uTw2sw2TE2+UcY1RiyBZkaGbTkVg9SSdPNUG6aUw=="], + "typescript-eslint": ["typescript-eslint@8.46.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.46.1", "@typescript-eslint/parser": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1", "@typescript-eslint/utils": "8.46.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-VHgijW803JafdSsDO8I761r3SHrgk4T00IdyQ+/UsthtgPRsBWQLqoSxOolxTpxRKi1kGXK0bSz4CoAc9ObqJA=="], "undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], @@ -899,7 +899,7 @@ "vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="], - "vite": ["vite@7.1.9", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg=="], + "vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="], "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], @@ -971,25 +971,25 @@ "@types/estree-jsx/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], - "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.0", "", { "dependencies": { "@typescript-eslint/types": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0" } }, "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1" } }, "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A=="], - "@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", "@typescript-eslint/typescript-estree": "8.46.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.4", "", {}, "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="], - "@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.0", "", { "dependencies": { "@typescript-eslint/types": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0" } }, "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw=="], + "@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1" } }, "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A=="], - "@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="], + "@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.46.1", "", {}, "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ=="], - "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="], + "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.46.1", "", {}, "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ=="], "@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.45.0", "", { "dependencies": { "@typescript-eslint/types": "8.45.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag=="], - "@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="], + "@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.46.1", "", {}, "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ=="], - "@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", "@typescript-eslint/typescript-estree": "8.46.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g=="], + "@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ=="], - "@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="], + "@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.46.1", "", {}, "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -997,7 +997,7 @@ "@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.45.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.45.0", "@typescript-eslint/tsconfig-utils": "8.45.0", "@typescript-eslint/types": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA=="], - "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="], + "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.46.1", "", {}, "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -1013,7 +1013,7 @@ "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - "typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", "@typescript-eslint/typescript-estree": "8.46.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g=="], + "typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ=="], "@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.27.1", "", { "dependencies": { "@babel/parser": "^7.27.1", "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w=="], @@ -1029,11 +1029,11 @@ "@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], - "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.46.1", "", {}, "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ=="], - "@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.46.1", "", {}, "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ=="], - "@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.0", "", { "dependencies": { "@typescript-eslint/types": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0" } }, "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw=="], + "@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1" } }, "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], @@ -1047,9 +1047,9 @@ "@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], - "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.0", "", { "dependencies": { "@typescript-eslint/types": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0" } }, "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw=="], + "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1" } }, "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A=="], - "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="], + "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.46.1", "", {}, "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ=="], "@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], diff --git a/frontend/package.json b/frontend/package.json index 74671191..a0449f13 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,7 @@ "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@tailwindcss/vite": "^4.1.14", - "@tanstack/react-query": "^5.90.2", + "@tanstack/react-query": "^5.90.3", "axios": "^1.12.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -29,7 +29,7 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "react-hook-form": "^7.65.0", - "react-i18next": "^16.0.0", + "react-i18next": "^16.0.1", "react-markdown": "^10.1.0", "react-router": "^7.9.4", "sonner": "^2.0.7", @@ -42,7 +42,7 @@ "@tanstack/eslint-plugin-query": "^5.91.0", "@types/node": "^24.7.2", "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.1", + "@types/react-dom": "^19.2.2", "@vitejs/plugin-react": "^5.0.4", "eslint": "^9.37.0", "eslint-plugin-react-hooks": "^7.0.0", @@ -51,7 +51,7 @@ "prettier": "3.6.2", "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", - "typescript-eslint": "^8.46.0", - "vite": "^7.1.9" + "typescript-eslint": "^8.46.1", + "vite": "^7.1.10" } } \ No newline at end of file From 548243090747019d48ac8306e271d64399c325dd Mon Sep 17 00:00:00 2001 From: Stavros Date: Sun, 19 Oct 2025 19:03:32 +0300 Subject: [PATCH 093/127] refactor: generate a verifier on every oauth auth session --- internal/controller/oauth_controller.go | 1 + internal/service/generic_oauth_service.go | 8 ++++++-- internal/service/github_oauth_service.go | 9 ++++++--- internal/service/google_oauth_service.go | 9 ++++++--- internal/service/oauth_broker_service.go | 1 + 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/internal/controller/oauth_controller.go b/internal/controller/oauth_controller.go index c0a7380b..61b6c078 100644 --- a/internal/controller/oauth_controller.go +++ b/internal/controller/oauth_controller.go @@ -72,6 +72,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) { return } + service.GenerateVerifier() state := service.GenerateState() authURL := service.GetAuthURL(state) c.SetCookie(controller.config.CSRFCookieName, state, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true) diff --git a/internal/service/generic_oauth_service.go b/internal/service/generic_oauth_service.go index 053944ad..49fa9bd3 100644 --- a/internal/service/generic_oauth_service.go +++ b/internal/service/generic_oauth_service.go @@ -59,10 +59,8 @@ func (generic *GenericOAuthService) Init() error { ctx := context.Background() ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) - verifier := oauth2.GenerateVerifier() generic.context = ctx - generic.verifier = verifier return nil } @@ -76,6 +74,12 @@ func (generic *GenericOAuthService) GenerateState() string { return state } +func (generic *GenericOAuthService) GenerateVerifier() string { + verifier := oauth2.GenerateVerifier() + generic.verifier = verifier + return verifier +} + func (generic *GenericOAuthService) GetAuthURL(state string) string { return generic.config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(generic.verifier)) } diff --git a/internal/service/github_oauth_service.go b/internal/service/github_oauth_service.go index 163c2c88..0d3d76fa 100644 --- a/internal/service/github_oauth_service.go +++ b/internal/service/github_oauth_service.go @@ -53,10 +53,7 @@ func (github *GithubOAuthService) Init() error { httpClient := &http.Client{} ctx := context.Background() ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) - verifier := oauth2.GenerateVerifier() - github.context = ctx - github.verifier = verifier return nil } @@ -70,6 +67,12 @@ func (github *GithubOAuthService) GenerateState() string { return state } +func (github *GithubOAuthService) GenerateVerifier() string { + verifier := oauth2.GenerateVerifier() + github.verifier = verifier + return verifier +} + func (github *GithubOAuthService) GetAuthURL(state string) string { return github.config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(github.verifier)) } diff --git a/internal/service/google_oauth_service.go b/internal/service/google_oauth_service.go index ab0597d3..474c2851 100644 --- a/internal/service/google_oauth_service.go +++ b/internal/service/google_oauth_service.go @@ -48,10 +48,7 @@ func (google *GoogleOAuthService) Init() error { httpClient := &http.Client{} ctx := context.Background() ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) - verifier := oauth2.GenerateVerifier() - google.context = ctx - google.verifier = verifier return nil } @@ -65,6 +62,12 @@ func (oauth *GoogleOAuthService) GenerateState() string { return state } +func (google *GoogleOAuthService) GenerateVerifier() string { + verifier := oauth2.GenerateVerifier() + google.verifier = verifier + return verifier +} + func (google *GoogleOAuthService) GetAuthURL(state string) string { return google.config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(google.verifier)) } diff --git a/internal/service/oauth_broker_service.go b/internal/service/oauth_broker_service.go index 10381841..e15d9c69 100644 --- a/internal/service/oauth_broker_service.go +++ b/internal/service/oauth_broker_service.go @@ -11,6 +11,7 @@ import ( type OAuthService interface { Init() error GenerateState() string + GenerateVerifier() string GetAuthURL(state string) string VerifyCode(code string) error Userinfo() (config.Claims, error) From 7231efcbc3adbdbe06f900efdf0f470a6f086869 Mon Sep 17 00:00:00 2001 From: Stavros Date: Sun, 19 Oct 2025 19:10:24 +0300 Subject: [PATCH 094/127] feat: add routine to cleanup expired sessions --- internal/bootstrap/app_bootstrap.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index 2cb7e979..498b41c1 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -2,6 +2,7 @@ package bootstrap import ( "bytes" + "context" "encoding/json" "fmt" "net/http" @@ -13,11 +14,13 @@ import ( "tinyauth/internal/config" "tinyauth/internal/controller" "tinyauth/internal/middleware" + "tinyauth/internal/model" "tinyauth/internal/service" "tinyauth/internal/utils" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" + "gorm.io/gorm" ) type Controller interface { @@ -277,6 +280,10 @@ func (app *BootstrapApp) Setup() error { go app.heartbeat() } + // Start DB cleanup routine + log.Debug().Msg("Starting database cleanup routine") + go app.dbCleanup(database) + // Start server address := fmt.Sprintf("%s:%d", app.config.Address, app.config.Port) log.Info().Msgf("Starting server on %s", address) @@ -338,3 +345,17 @@ func (app *BootstrapApp) heartbeat() { } } } + +func (app *BootstrapApp) dbCleanup(db *gorm.DB) { + ticker := time.NewTicker(time.Duration(30) * time.Minute) + defer ticker.Stop() + ctx := context.Background() + + for ; true; <-ticker.C { + log.Debug().Msg("Cleaning up old database sessions") + _, err := gorm.G[model.Session](db).Where("expiry < ?", time.Now().UnixMilli()).Delete(ctx) + if err != nil { + log.Error().Err(err).Msg("Failed to cleanup old sessions") + } + } +} From 6647c6cd78d14ce9809719db77daca266fa8c060 Mon Sep 17 00:00:00 2001 From: Stavros Date: Sun, 19 Oct 2025 19:16:53 +0300 Subject: [PATCH 095/127] refactor: use gorm generics api for database actions --- internal/service/auth_service.go | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index d9f792b0..afcd1857 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -1,6 +1,8 @@ package service import ( + "context" + "errors" "fmt" "regexp" "strings" @@ -41,6 +43,7 @@ type AuthService struct { loginMutex sync.RWMutex ldap *LdapService database *gorm.DB + ctx context.Context } func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, database *gorm.DB) *AuthService { @@ -54,6 +57,7 @@ func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapS } func (auth *AuthService) Init() error { + auth.ctx = context.Background() return nil } @@ -213,7 +217,7 @@ func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.Sessio OAuthName: data.OAuthName, } - err = auth.database.Create(&session).Error + err = gorm.G[model.Session](auth.database).Create(auth.ctx, &session) if err != nil { return err @@ -231,10 +235,10 @@ func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error { return err } - res := auth.database.Unscoped().Where("uuid = ?", cookie).Delete(&model.Session{}) + _, err = gorm.G[model.Session](auth.database).Where("uuid = ?", cookie).Delete(auth.ctx) - if res.Error != nil { - return res.Error + if err != nil { + return err } c.SetCookie(auth.config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true) @@ -249,15 +253,13 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie, return config.SessionCookie{}, err } - var session model.Session - - res := auth.database.Unscoped().Where("uuid = ?", cookie).First(&session) + session, err := gorm.G[model.Session](auth.database).Where("uuid = ?", cookie).First(auth.ctx) - if res.Error != nil { - return config.SessionCookie{}, res.Error + if err != nil { + return config.SessionCookie{}, err } - if res.RowsAffected == 0 { + if errors.Is(err, gorm.ErrRecordNotFound) { return config.SessionCookie{}, fmt.Errorf("session not found") } From c5bb389258ef459315b5f0c2163988eebea2fa83 Mon Sep 17 00:00:00 2001 From: Chris Ellrich <36817049+chrellrich@users.noreply.github.com> Date: Tue, 21 Oct 2025 15:02:31 +0200 Subject: [PATCH 096/127] feat: ACL labels from environment variables (#422) * feat: add LabelService to retrieve application labels from environment variables * feat: allow usage of labels from docker and env variables simultaneously Prioritize labels from environment variables over labels from docker labels * fix: handle error returned by label_serive.go/LoadLabels see https://github.com/steveiliop56/tinyauth/pull/422#discussion_r2443443032 * refactor(label_service): use simple loop instead of slices.ContainsFunc to avoid experimental slices package see https://github.com/steveiliop56/tinyauth/pull/422#pullrequestreview-3354632045 * refactor: merge acl logic into one service --------- Co-authored-by: Stavros --- internal/bootstrap/app_bootstrap.go | 6 +- internal/controller/proxy_controller.go | 41 ++++---- internal/controller/proxy_controller_test.go | 7 +- internal/service/access_controls_service.go | 103 +++++++++++++++++++ internal/service/auth_service.go | 22 ++-- 5 files changed, 145 insertions(+), 34 deletions(-) create mode 100644 internal/service/access_controls_service.go diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index 498b41c1..fdbd3827 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -139,12 +139,14 @@ func (app *BootstrapApp) Setup() error { // Create services dockerService := service.NewDockerService() + aclsService := service.NewAccessControlsService(dockerService) authService := service.NewAuthService(authConfig, dockerService, ldapService, database) oauthBrokerService := service.NewOAuthBrokerService(oauthProviders) - // Initialize services + // Initialize services (order matters) services := []Service{ dockerService, + aclsService, authService, oauthBrokerService, } @@ -246,7 +248,7 @@ func (app *BootstrapApp) Setup() error { proxyController := controller.NewProxyController(controller.ProxyControllerConfig{ AppURL: app.config.AppURL, - }, apiRouter, dockerService, authService) + }, apiRouter, aclsService, authService) userController := controller.NewUserController(controller.UserControllerConfig{ CookieDomain: cookieDomain, diff --git a/internal/controller/proxy_controller.go b/internal/controller/proxy_controller.go index 8ded9dcd..2b6738ac 100644 --- a/internal/controller/proxy_controller.go +++ b/internal/controller/proxy_controller.go @@ -24,15 +24,15 @@ type ProxyControllerConfig struct { type ProxyController struct { config ProxyControllerConfig router *gin.RouterGroup - docker *service.DockerService + acls *service.AccessControlsService auth *service.AuthService } -func NewProxyController(config ProxyControllerConfig, router *gin.RouterGroup, docker *service.DockerService, auth *service.AuthService) *ProxyController { +func NewProxyController(config ProxyControllerConfig, router *gin.RouterGroup, acls *service.AccessControlsService, auth *service.AuthService) *ProxyController { return &ProxyController{ config: config, router: router, - docker: docker, + acls: acls, auth: auth, } } @@ -76,20 +76,21 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { proto := c.Request.Header.Get("X-Forwarded-Proto") host := c.Request.Header.Get("X-Forwarded-Host") - labels, err := controller.docker.GetLabels(host) + // Get acls + acls, err := controller.acls.GetAccessControls(host) if err != nil { - log.Error().Err(err).Msg("Failed to get labels from Docker") + log.Error().Err(err).Msg("Failed to get access controls for resource") controller.handleError(c, req, isBrowser) return } - log.Trace().Interface("labels", labels).Msg("Labels for resource") + log.Trace().Interface("acls", acls).Msg("ACLs for resource") clientIP := c.ClientIP() - if controller.auth.IsBypassedIP(labels.IP, clientIP) { - controller.setHeaders(c, labels) + if controller.auth.IsBypassedIP(acls.IP, clientIP) { + controller.setHeaders(c, acls) c.JSON(200, gin.H{ "status": 200, "message": "Authenticated", @@ -97,7 +98,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { return } - authEnabled, err := controller.auth.IsAuthEnabled(uri, labels.Path) + authEnabled, err := controller.auth.IsAuthEnabled(uri, acls.Path) if err != nil { log.Error().Err(err).Msg("Failed to check if auth is enabled for resource") @@ -107,7 +108,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { if !authEnabled { log.Debug().Msg("Authentication disabled for resource, allowing access") - controller.setHeaders(c, labels) + controller.setHeaders(c, acls) c.JSON(200, gin.H{ "status": 200, "message": "Authenticated", @@ -115,7 +116,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { return } - if !controller.auth.CheckIP(labels.IP, clientIP) { + if !controller.auth.CheckIP(acls.IP, clientIP) { if req.Proxy == "nginx" || !isBrowser { c.JSON(401, gin.H{ "status": 401, @@ -160,7 +161,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { } if userContext.IsLoggedIn { - appAllowed := controller.auth.IsResourceAllowed(c, userContext, labels) + appAllowed := controller.auth.IsResourceAllowed(c, userContext, acls) if !appAllowed { log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource") @@ -194,7 +195,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { } if userContext.OAuth { - groupOK := controller.auth.IsInOAuthGroup(c, userContext, labels.OAuth.Groups) + groupOK := controller.auth.IsInOAuthGroup(c, userContext, acls.OAuth.Groups) if !groupOK { log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements") @@ -234,7 +235,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { c.Header("Remote-Email", utils.SanitizeHeader(userContext.Email)) c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups)) - controller.setHeaders(c, labels) + controller.setHeaders(c, acls) c.JSON(200, gin.H{ "status": 200, @@ -264,21 +265,21 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/login?%s", controller.config.AppURL, queries.Encode())) } -func (controller *ProxyController) setHeaders(c *gin.Context, labels config.App) { +func (controller *ProxyController) setHeaders(c *gin.Context, acls config.App) { c.Header("Authorization", c.Request.Header.Get("Authorization")) - headers := utils.ParseHeaders(labels.Response.Headers) + headers := utils.ParseHeaders(acls.Response.Headers) for key, value := range headers { log.Debug().Str("header", key).Msg("Setting header") c.Header(key, value) } - basicPassword := utils.GetSecret(labels.Response.BasicAuth.Password, labels.Response.BasicAuth.PasswordFile) + basicPassword := utils.GetSecret(acls.Response.BasicAuth.Password, acls.Response.BasicAuth.PasswordFile) - if labels.Response.BasicAuth.Username != "" && basicPassword != "" { - log.Debug().Str("username", labels.Response.BasicAuth.Username).Msg("Setting basic auth header") - c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(labels.Response.BasicAuth.Username, basicPassword))) + if acls.Response.BasicAuth.Username != "" && basicPassword != "" { + log.Debug().Str("username", acls.Response.BasicAuth.Username).Msg("Setting basic auth header") + c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(acls.Response.BasicAuth.Username, basicPassword))) } } diff --git a/internal/controller/proxy_controller_test.go b/internal/controller/proxy_controller_test.go index fce2ec38..e7e27cf0 100644 --- a/internal/controller/proxy_controller_test.go +++ b/internal/controller/proxy_controller_test.go @@ -39,6 +39,11 @@ func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.En assert.NilError(t, dockerService.Init()) + // Access controls + accessControlsService := service.NewAccessControlsService(dockerService) + + assert.NilError(t, accessControlsService.Init()) + // Auth service authService := service.NewAuthService(service.AuthServiceConfig{ Users: []config.User{ @@ -59,7 +64,7 @@ func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.En // Controller ctrl := controller.NewProxyController(controller.ProxyControllerConfig{ AppURL: "http://localhost:8080", - }, group, dockerService, authService) + }, group, accessControlsService, authService) ctrl.SetupRoutes() return router, recorder, authService diff --git a/internal/service/access_controls_service.go b/internal/service/access_controls_service.go new file mode 100644 index 00000000..cde27e50 --- /dev/null +++ b/internal/service/access_controls_service.go @@ -0,0 +1,103 @@ +package service + +import ( + "os" + "strings" + "tinyauth/internal/config" + "tinyauth/internal/utils/decoders" + + "github.com/rs/zerolog/log" +) + +type AccessControlsService struct { + docker *DockerService + envACLs config.Apps +} + +func NewAccessControlsService(docker *DockerService) *AccessControlsService { + return &AccessControlsService{ + docker: docker, + } +} + +func (acls *AccessControlsService) Init() error { + acls.envACLs = config.Apps{} + env := os.Environ() + appEnvVars := []string{} + + for _, e := range env { + if strings.HasPrefix(e, "TINYAUTH_APPS_") { + appEnvVars = append(appEnvVars, e) + } + } + + err := acls.loadEnvACLs(appEnvVars) + + if err != nil { + return err + } + + return nil +} + +func (acls *AccessControlsService) loadEnvACLs(appEnvVars []string) error { + if len(appEnvVars) == 0 { + return nil + } + + envAcls := map[string]string{} + + for _, e := range appEnvVars { + parts := strings.SplitN(e, "=", 2) + if len(parts) != 2 { + continue + } + + // Normalize key, this should use the same normalization logic as in utils/decoders/decoders.go + key := parts[0] + key = strings.ToLower(key) + key = strings.ReplaceAll(key, "_", ".") + value := parts[1] + envAcls[key] = value + } + + apps, err := decoders.DecodeLabels(envAcls) + + if err != nil { + return err + } + + acls.envACLs = apps + return nil +} + +func (acls *AccessControlsService) lookupEnvACLs(appDomain string) *config.App { + if len(acls.envACLs.Apps) == 0 { + return nil + } + + for appName, appACLs := range acls.envACLs.Apps { + if appACLs.Config.Domain == appDomain { + return &appACLs + } + + if strings.SplitN(appDomain, ".", 2)[0] == appName { + return &appACLs + } + } + + return nil +} + +func (acls *AccessControlsService) GetAccessControls(appDomain string) (config.App, error) { + // First check environment variables + envACLs := acls.lookupEnvACLs(appDomain) + + if envACLs != nil { + log.Debug().Str("domain", appDomain).Msg("Found matching access controls in environment variables") + return *envACLs, nil + } + + // Fallback to Docker labels + return acls.docker.GetLabels(appDomain) +} diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index afcd1857..a75bb04e 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -289,21 +289,21 @@ func (auth *AuthService) UserAuthConfigured() bool { return len(auth.config.Users) > 0 || auth.ldap != nil } -func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, labels config.App) bool { +func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, acls config.App) bool { if context.OAuth { log.Debug().Msg("Checking OAuth whitelist") - return utils.CheckFilter(labels.OAuth.Whitelist, context.Email) + return utils.CheckFilter(acls.OAuth.Whitelist, context.Email) } - if labels.Users.Block != "" { + if acls.Users.Block != "" { log.Debug().Msg("Checking blocked users") - if utils.CheckFilter(labels.Users.Block, context.Username) { + if utils.CheckFilter(acls.Users.Block, context.Username) { return false } } log.Debug().Msg("Checking users") - return utils.CheckFilter(labels.Users.Allow, context.Username) + return utils.CheckFilter(acls.Users.Allow, context.Username) } func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserContext, requiredGroups string) bool { @@ -371,8 +371,8 @@ func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User { } } -func (auth *AuthService) CheckIP(labels config.AppIP, ip string) bool { - for _, blocked := range labels.Block { +func (auth *AuthService) CheckIP(acls config.AppIP, ip string) bool { + for _, blocked := range acls.Block { res, err := utils.FilterIP(blocked, ip) if err != nil { log.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list") @@ -384,7 +384,7 @@ func (auth *AuthService) CheckIP(labels config.AppIP, ip string) bool { } } - for _, allowed := range labels.Allow { + for _, allowed := range acls.Allow { res, err := utils.FilterIP(allowed, ip) if err != nil { log.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list") @@ -396,7 +396,7 @@ func (auth *AuthService) CheckIP(labels config.AppIP, ip string) bool { } } - if len(labels.Allow) > 0 { + if len(acls.Allow) > 0 { log.Debug().Str("ip", ip).Msg("IP not in allow list, denying access") return false } @@ -405,8 +405,8 @@ func (auth *AuthService) CheckIP(labels config.AppIP, ip string) bool { return true } -func (auth *AuthService) IsBypassedIP(labels config.AppIP, ip string) bool { - for _, bypassed := range labels.Bypass { +func (auth *AuthService) IsBypassedIP(acls config.AppIP, ip string) bool { + for _, bypassed := range acls.Bypass { res, err := utils.FilterIP(bypassed, ip) if err != nil { log.Warn().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list") From 0227af6d2b8408a7f31afc27a328fe107e208920 Mon Sep 17 00:00:00 2001 From: Stavros Date: Sun, 26 Oct 2025 12:01:19 +0200 Subject: [PATCH 097/127] refactor: rework decoders logic for cleaner code (#431) * refactor: rework decoders logic for cleaner code * refactor: use strcase lib to handle text case conversions --- go.mod | 1 + go.sum | 2 + internal/config/config.go | 20 ++--- internal/utils/app_utils.go | 4 +- internal/utils/decoders/decoders.go | 83 ++++++++----------- internal/utils/decoders/decoders_test.go | 49 ----------- internal/utils/decoders/env_decoder.go | 15 ++-- internal/utils/decoders/env_decoder_test.go | 57 ++++--------- internal/utils/decoders/flags_decoder.go | 14 ++-- internal/utils/decoders/flags_decoder_test.go | 57 ++++--------- 10 files changed, 98 insertions(+), 204 deletions(-) delete mode 100644 internal/utils/decoders/decoders_test.go diff --git a/go.mod b/go.mod index 12b29b91..23488759 100644 --- a/go.mod +++ b/go.mod @@ -47,6 +47,7 @@ require ( github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/quic-go v0.54.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/stoewer/go-strcase v1.3.1 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect diff --git a/go.sum b/go.sum index be59848a..741a59fb 100644 --- a/go.sum +++ b/go.sum @@ -259,6 +259,8 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= +github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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= diff --git a/internal/config/config.go b/internal/config/config.go index 23c38325..40c97518 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -53,16 +53,16 @@ type Claims struct { } type OAuthServiceConfig struct { - ClientID string `key:"client-id"` - ClientSecret string `key:"client-secret"` - ClientSecretFile string `key:"client-secret-file"` - Scopes []string `key:"scopes"` - RedirectURL string `key:"redirect-url"` - AuthURL string `key:"auth-url"` - TokenURL string `key:"token-url"` - UserinfoURL string `key:"user-info-url"` - InsecureSkipVerify bool `key:"insecure-skip-verify"` - Name string `key:"name"` + ClientID string `field:"client-id"` + ClientSecret string + ClientSecretFile string + Scopes []string + RedirectURL string `field:"redirect-url"` + AuthURL string `field:"auth-url"` + TokenURL string `field:"token-url"` + UserinfoURL string `field:"user-info-url"` + InsecureSkipVerify bool + Name string } var OverrideProviders = map[string]string{ diff --git a/internal/utils/app_utils.go b/internal/utils/app_utils.go index 7d143ac4..76044c95 100644 --- a/internal/utils/app_utils.go +++ b/internal/utils/app_utils.go @@ -147,7 +147,7 @@ func GetOAuthProvidersConfig(env []string, args []string, appUrl string) (map[st } } - envProviders, err := decoders.DecodeEnv(envMap) + envProviders, err := decoders.DecodeEnv[config.Providers, config.OAuthServiceConfig](envMap, "providers") if err != nil { return nil, err @@ -167,7 +167,7 @@ func GetOAuthProvidersConfig(env []string, args []string, appUrl string) (map[st } } - flagProviders, err := decoders.DecodeFlags(flagsMap) + flagProviders, err := decoders.DecodeFlags[config.Providers, config.OAuthServiceConfig](flagsMap, "providers") if err != nil { return nil, err diff --git a/internal/utils/decoders/decoders.go b/internal/utils/decoders/decoders.go index 63604b14..28b72fb3 100644 --- a/internal/utils/decoders/decoders.go +++ b/internal/utils/decoders/decoders.go @@ -3,29 +3,24 @@ package decoders import ( "reflect" "strings" - "tinyauth/internal/config" + + "github.com/stoewer/go-strcase" ) -func NormalizeKeys(keys map[string]string, rootName string, sep string) map[string]string { +func normalizeKeys[T any](input map[string]string, root string, sep string) map[string]string { + knownKeys := getKnownKeys[T]() normalized := make(map[string]string) - knownKeys := getKnownKeys() - for k, v := range keys { - var finalKey []string - var suffix string - var camelClientName string - var camelField string + for k, v := range input { + parts := []string{"tinyauth"} - finalKey = append(finalKey, rootName) - finalKey = append(finalKey, "providers") - lowerKey := strings.ToLower(k) + key := strings.ToLower(k) + key = strings.ReplaceAll(key, sep, "-") - if !strings.HasPrefix(lowerKey, "providers"+sep) { - continue - } + suffix := "" for _, known := range knownKeys { - if strings.HasSuffix(lowerKey, strings.ReplaceAll(known, "-", sep)) { + if strings.HasSuffix(key, known) { suffix = known break } @@ -35,55 +30,47 @@ func NormalizeKeys(keys map[string]string, rootName string, sep string) map[stri continue } - if strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(lowerKey, "providers"+sep), strings.ReplaceAll(suffix, "-", sep))) == "" { - continue - } + parts = append(parts, root) - clientNameParts := strings.Split(strings.TrimPrefix(strings.TrimSuffix(lowerKey, sep+strings.ReplaceAll(suffix, "-", sep)), "providers"+sep), sep) + id := strings.TrimPrefix(key, root+"-") + id = strings.TrimSuffix(id, "-"+suffix) - for i, p := range clientNameParts { - if i == 0 { - camelClientName += p - continue - } - if p == "" { - continue - } - camelClientName += strings.ToUpper(string([]rune(p)[0])) + string([]rune(p)[1:]) + if id == "" { + continue } - finalKey = append(finalKey, camelClientName) + parts = append(parts, id) + parts = append(parts, suffix) - fieldParts := strings.Split(suffix, "-") + final := "" - for i, p := range fieldParts { - if i == 0 { - camelField += p - continue - } - if p == "" { - continue + for i, part := range parts { + if i > 0 { + final += "." } - camelField += strings.ToUpper(string([]rune(p)[0])) + string([]rune(p)[1:]) + final += strcase.LowerCamelCase(part) } - finalKey = append(finalKey, camelField) - normalized[strings.Join(finalKey, ".")] = v + normalized[final] = v } return normalized } -func getKnownKeys() []string { - var known []string +func getKnownKeys[T any]() []string { + var keys []string + var t T - p := config.OAuthServiceConfig{} - v := reflect.ValueOf(p) - typeOfP := v.Type() + v := reflect.ValueOf(t) + typeOfT := v.Type() - for field := range typeOfP.NumField() { - known = append(known, typeOfP.Field(field).Tag.Get("key")) + for field := range typeOfT.NumField() { + if typeOfT.Field(field).Tag.Get("field") != "" { + keys = append(keys, typeOfT.Field(field).Tag.Get("field")) + continue + } + keys = append(keys, strcase.KebabCase(typeOfT.Field(field).Name)) } - return known + return keys } diff --git a/internal/utils/decoders/decoders_test.go b/internal/utils/decoders/decoders_test.go deleted file mode 100644 index fdec2869..00000000 --- a/internal/utils/decoders/decoders_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package decoders_test - -import ( - "testing" - "tinyauth/internal/utils/decoders" - - "gotest.tools/v3/assert" -) - -func TestNormalizeKeys(t *testing.T) { - // Test with env - test := map[string]string{ - "PROVIDERS_CLIENT1_CLIENT_ID": "my-client-id", - "PROVIDERS_CLIENT1_CLIENT_SECRET": "my-client-secret", - "PROVIDERS_MY_AWESOME_CLIENT_CLIENT_ID": "my-awesome-client-id", - "PROVIDERS_MY_AWESOME_CLIENT_CLIENT_SECRET_FILE": "/path/to/secret", - "I_LOOK_LIKE_A_KEY_CLIENT_ID": "should-not-appear", - "PROVIDERS_CLIENT_ID": "should-not-appear", - } - expected := map[string]string{ - "tinyauth.providers.client1.clientId": "my-client-id", - "tinyauth.providers.client1.clientSecret": "my-client-secret", - "tinyauth.providers.myAwesomeClient.clientId": "my-awesome-client-id", - "tinyauth.providers.myAwesomeClient.clientSecretFile": "/path/to/secret", - } - - normalized := decoders.NormalizeKeys(test, "tinyauth", "_") - assert.DeepEqual(t, normalized, expected) - - // Test with flags (assume -- is already stripped) - test = map[string]string{ - "providers-client1-client-id": "my-client-id", - "providers-client1-client-secret": "my-client-secret", - "providers-my-awesome-client-client-id": "my-awesome-client-id", - "providers-my-awesome-client-client-secret-file": "/path/to/secret", - "providers-should-not-appear-client": "should-not-appear", - "i-look-like-a-key-client-id": "should-not-appear", - "providers-client-id": "should-not-appear", - } - expected = map[string]string{ - "tinyauth.providers.client1.clientId": "my-client-id", - "tinyauth.providers.client1.clientSecret": "my-client-secret", - "tinyauth.providers.myAwesomeClient.clientId": "my-awesome-client-id", - "tinyauth.providers.myAwesomeClient.clientSecretFile": "/path/to/secret", - } - - normalized = decoders.NormalizeKeys(test, "tinyauth", "-") - assert.DeepEqual(t, normalized, expected) -} diff --git a/internal/utils/decoders/env_decoder.go b/internal/utils/decoders/env_decoder.go index 4164aa55..532ec648 100644 --- a/internal/utils/decoders/env_decoder.go +++ b/internal/utils/decoders/env_decoder.go @@ -1,20 +1,19 @@ package decoders import ( - "tinyauth/internal/config" - "github.com/traefik/paerser/parser" ) -func DecodeEnv(env map[string]string) (config.Providers, error) { - normalized := NormalizeKeys(env, "tinyauth", "_") - var providers config.Providers +func DecodeEnv[T any, C any](env map[string]string, subName string) (T, error) { + var result T + + normalized := normalizeKeys[C](env, subName, "_") - err := parser.Decode(normalized, &providers, "tinyauth", "tinyauth.providers") + err := parser.Decode(normalized, &result, "tinyauth", "tinyauth."+subName) if err != nil { - return config.Providers{}, err + return result, err } - return providers, nil + return result, nil } diff --git a/internal/utils/decoders/env_decoder_test.go b/internal/utils/decoders/env_decoder_test.go index 2233241f..da679f0e 100644 --- a/internal/utils/decoders/env_decoder_test.go +++ b/internal/utils/decoders/env_decoder_test.go @@ -9,52 +9,29 @@ import ( ) func TestDecodeEnv(t *testing.T) { - // Variables + // Setup + env := map[string]string{ + "PROVIDERS_GOOGLE_CLIENT_ID": "google-client-id", + "PROVIDERS_GOOGLE_CLIENT_SECRET": "google-client-secret", + "PROVIDERS_MY_GITHUB_CLIENT_ID": "github-client-id", + "PROVIDERS_MY_GITHUB_CLIENT_SECRET": "github-client-secret", + } + expected := config.Providers{ Providers: map[string]config.OAuthServiceConfig{ - "client1": { - ClientID: "client1-id", - ClientSecret: "client1-secret", - Scopes: []string{"client1-scope1", "client1-scope2"}, - RedirectURL: "client1-redirect-url", - AuthURL: "client1-auth-url", - UserinfoURL: "client1-user-info-url", - Name: "Client1", - InsecureSkipVerify: false, + "google": { + ClientID: "google-client-id", + ClientSecret: "google-client-secret", }, - "client2": { - ClientID: "client2-id", - ClientSecret: "client2-secret", - Scopes: []string{"client2-scope1", "client2-scope2"}, - RedirectURL: "client2-redirect-url", - AuthURL: "client2-auth-url", - UserinfoURL: "client2-user-info-url", - Name: "My Awesome Client2", - InsecureSkipVerify: false, + "myGithub": { + ClientID: "github-client-id", + ClientSecret: "github-client-secret", }, }, } - test := map[string]string{ - "PROVIDERS_CLIENT1_CLIENT_ID": "client1-id", - "PROVIDERS_CLIENT1_CLIENT_SECRET": "client1-secret", - "PROVIDERS_CLIENT1_SCOPES": "client1-scope1,client1-scope2", - "PROVIDERS_CLIENT1_REDIRECT_URL": "client1-redirect-url", - "PROVIDERS_CLIENT1_AUTH_URL": "client1-auth-url", - "PROVIDERS_CLIENT1_USER_INFO_URL": "client1-user-info-url", - "PROVIDERS_CLIENT1_NAME": "Client1", - "PROVIDERS_CLIENT1_INSECURE_SKIP_VERIFY": "false", - "PROVIDERS_CLIENT2_CLIENT_ID": "client2-id", - "PROVIDERS_CLIENT2_CLIENT_SECRET": "client2-secret", - "PROVIDERS_CLIENT2_SCOPES": "client2-scope1,client2-scope2", - "PROVIDERS_CLIENT2_REDIRECT_URL": "client2-redirect-url", - "PROVIDERS_CLIENT2_AUTH_URL": "client2-auth-url", - "PROVIDERS_CLIENT2_USER_INFO_URL": "client2-user-info-url", - "PROVIDERS_CLIENT2_NAME": "My Awesome Client2", - "PROVIDERS_CLIENT2_INSECURE_SKIP_VERIFY": "false", - } - // Test - res, err := decoders.DecodeEnv(test) + // Execute + result, err := decoders.DecodeEnv[config.Providers, config.OAuthServiceConfig](env, "providers") assert.NilError(t, err) - assert.DeepEqual(t, expected, res) + assert.DeepEqual(t, result, expected) } diff --git a/internal/utils/decoders/flags_decoder.go b/internal/utils/decoders/flags_decoder.go index d973d299..0aae2341 100644 --- a/internal/utils/decoders/flags_decoder.go +++ b/internal/utils/decoders/flags_decoder.go @@ -2,23 +2,23 @@ package decoders import ( "strings" - "tinyauth/internal/config" "github.com/traefik/paerser/parser" ) -func DecodeFlags(flags map[string]string) (config.Providers, error) { +func DecodeFlags[T any, C any](flags map[string]string, subName string) (T, error) { + var result T + filtered := filterFlags(flags) - normalized := NormalizeKeys(filtered, "tinyauth", "-") - var providers config.Providers + normalized := normalizeKeys[C](filtered, subName, "_") - err := parser.Decode(normalized, &providers, "tinyauth", "tinyauth.providers") + err := parser.Decode(normalized, &result, "tinyauth", "tinyauth."+subName) if err != nil { - return config.Providers{}, err + return result, err } - return providers, nil + return result, nil } func filterFlags(flags map[string]string) map[string]string { diff --git a/internal/utils/decoders/flags_decoder_test.go b/internal/utils/decoders/flags_decoder_test.go index 356b4ae7..935dea0f 100644 --- a/internal/utils/decoders/flags_decoder_test.go +++ b/internal/utils/decoders/flags_decoder_test.go @@ -9,52 +9,29 @@ import ( ) func TestDecodeFlags(t *testing.T) { - // Variables + // Setup + flags := map[string]string{ + "--providers-google-client-id": "google-client-id", + "--providers-google-client-secret": "google-client-secret", + "--providers-my-github-client-id": "github-client-id", + "--providers-my-github-client-secret": "github-client-secret", + } + expected := config.Providers{ Providers: map[string]config.OAuthServiceConfig{ - "client1": { - ClientID: "client1-id", - ClientSecret: "client1-secret", - Scopes: []string{"client1-scope1", "client1-scope2"}, - RedirectURL: "client1-redirect-url", - AuthURL: "client1-auth-url", - UserinfoURL: "client1-user-info-url", - Name: "Client1", - InsecureSkipVerify: false, + "google": { + ClientID: "google-client-id", + ClientSecret: "google-client-secret", }, - "client2": { - ClientID: "client2-id", - ClientSecret: "client2-secret", - Scopes: []string{"client2-scope1", "client2-scope2"}, - RedirectURL: "client2-redirect-url", - AuthURL: "client2-auth-url", - UserinfoURL: "client2-user-info-url", - Name: "My Awesome Client2", - InsecureSkipVerify: false, + "myGithub": { + ClientID: "github-client-id", + ClientSecret: "github-client-secret", }, }, } - test := map[string]string{ - "--providers-client1-client-id": "client1-id", - "--providers-client1-client-secret": "client1-secret", - "--providers-client1-scopes": "client1-scope1,client1-scope2", - "--providers-client1-redirect-url": "client1-redirect-url", - "--providers-client1-auth-url": "client1-auth-url", - "--providers-client1-user-info-url": "client1-user-info-url", - "--providers-client1-name": "Client1", - "--providers-client1-insecure-skip-verify": "false", - "--providers-client2-client-id": "client2-id", - "--providers-client2-client-secret": "client2-secret", - "--providers-client2-scopes": "client2-scope1,client2-scope2", - "--providers-client2-redirect-url": "client2-redirect-url", - "--providers-client2-auth-url": "client2-auth-url", - "--providers-client2-user-info-url": "client2-user-info-url", - "--providers-client2-name": "My Awesome Client2", - "--providers-client2-insecure-skip-verify": "false", - } - // Test - res, err := decoders.DecodeFlags(test) + // Execute + result, err := decoders.DecodeFlags[config.Providers, config.OAuthServiceConfig](flags, "providers") assert.NilError(t, err) - assert.DeepEqual(t, expected, res) + assert.DeepEqual(t, result, expected) } From 330c7aa8f106f33fe3342a4adff902eed68f00f4 Mon Sep 17 00:00:00 2001 From: Stavros Date: Fri, 31 Oct 2025 15:55:50 +0200 Subject: [PATCH 098/127] feat: add support for light mode (#438) * feat: add support for light mode * refactor: use shadcn theme toggle * fix: fix sonner --- frontend/bun.lock | 9 + frontend/index.html | 2 +- frontend/package.json | 6 +- frontend/src/components/language/language.tsx | 3 +- frontend/src/components/layout/layout.tsx | 6 +- .../components/providers/theme-provider.tsx | 73 +++++ .../components/theme-toggle/theme-toggle.tsx | 40 +++ frontend/src/components/ui/button.tsx | 2 +- frontend/src/components/ui/dropdown-menu.tsx | 255 ++++++++++++++++++ frontend/src/components/ui/select.tsx | 2 +- frontend/src/components/ui/sonner.tsx | 4 +- frontend/src/main.tsx | 41 +-- 12 files changed, 415 insertions(+), 28 deletions(-) create mode 100644 frontend/src/components/providers/theme-provider.tsx create mode 100644 frontend/src/components/theme-toggle/theme-toggle.tsx create mode 100644 frontend/src/components/ui/dropdown-menu.tsx diff --git a/frontend/bun.lock b/frontend/bun.lock index 03217b35..dfad0877 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -5,6 +5,7 @@ "name": "tinyauth-shadcn", "dependencies": { "@hookform/resolvers": "^5.2.2", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", @@ -213,6 +214,8 @@ "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="], + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], @@ -221,12 +224,18 @@ "@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="], + "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="], + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="], + "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="], "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="], diff --git a/frontend/index.html b/frontend/index.html index 8078a279..04a8c04a 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -12,7 +12,7 @@ Tinyauth - +
diff --git a/frontend/package.json b/frontend/package.json index a0449f13..1317b4f5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,10 +7,12 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "tsc": "tsc -b" }, "dependencies": { "@hookform/resolvers": "^5.2.2", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", @@ -54,4 +56,4 @@ "typescript-eslint": "^8.46.1", "vite": "^7.1.10" } -} \ No newline at end of file +} diff --git a/frontend/src/components/language/language.tsx b/frontend/src/components/language/language.tsx index 875045cb..b38837ec 100644 --- a/frontend/src/components/language/language.tsx +++ b/frontend/src/components/language/language.tsx @@ -18,9 +18,10 @@ export const LanguageSelector = () => { setLanguage(option as SupportedLanguage); i18n.changeLanguage(option as SupportedLanguage); }; + return (