mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-10-31 14:15:50 +00:00 
			
		
		
		
	Compare commits
	
		
			179 Commits
		
	
	
		
			refactor/p
			...
			9a8b9aa93f
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 9a8b9aa93f | ||
|   | ba1436cc27 | ||
|   | 64cd37c26a | ||
|   | ea24aaa314 | ||
|   | a45bff8855 | ||
|   | 4e9342fa8b | ||
|   | 695e82d8e8 | ||
|   | d14b1efd1a | ||
|   | 8a2b7a1641 | ||
|   | 97214bc042 | ||
|   | 09061f698f | ||
|   | bf15538aca | ||
|   | 234e6b7d8b | ||
|   | 8a5d69927a | ||
|   | b99004607f | ||
|   | 0fa977f01c | ||
|   | 4a6dc8398b | ||
|   | 460eb6031b | ||
|   | c33ee12b1c | ||
|   | 72389f94b6 | ||
|   | 5a43ff79a5 | ||
|   | f388336201 | ||
|   | 67cb54453e | ||
|   | df1fe9a5ad | ||
|   | 0ce3f52688 | ||
|   | 4c11132176 | ||
|   | 2e1be914d4 | ||
|   | 455d253ebc | ||
|   | 17702efd3b | ||
|   | e89631e199 | ||
|   | 3d058570cc | ||
|   | 9ede29da3f | ||
|   | 13b9b38939 | ||
|   | 678317f802 | ||
|   | 59e1d314c5 | ||
|   | 81944e770e | ||
|   | c6ab05b151 | ||
|   | 744a33d264 | ||
|   | aa607eeb37 | ||
|   | d210a2bb67 | ||
|   | 37b3e74a43 | ||
|   | b8c7c47547 | ||
|   | 778fb63fed | ||
|   | 6313d05e7f | ||
|   | f118955cd7 | ||
|   | c4778cbfc1 | ||
|   | d200b54c4d | ||
|   | 163fad7c07 | ||
|   | 265042234b | ||
|   | b85f2daa1e | ||
|   | 85bbd62b5b | ||
|   | c2100ae939 | ||
|   | 968b6ce5a9 | ||
|   | d8783bdb5d | ||
|   | 46c6b297c0 | ||
|   | 458c8dd660 | ||
|   | b8abc852ee | ||
|   | edf4945f28 | ||
|   | e4e804cd37 | ||
|   | 8f17b57d11 | ||
|   | 7662737419 | ||
|   | ac3082aaaa | ||
|   | cd8513fea1 | ||
|   | ccbad25015 | ||
|   | 4de8109178 | ||
|   | 0abe2d4055 | ||
|   | 3e7b414022 | ||
|   | 949611f0a0 | ||
|   | 1aa3bc1fec | ||
|   | 0b5e562b73 | ||
|   | 9ea4250b95 | ||
|   | bafbb3e9a1 | ||
|   | fd7d997946 | ||
|   | 506e80ce20 | ||
|   | 3285476b23 | ||
|   | 3ee0f4c128 | ||
|   | 3b6ea5de8f | ||
|   | f1ef8b0c90 | ||
|   | 7c02ec4226 | ||
|   | aae2c5ea8d | ||
|   | 573f211286 | ||
|   | 3cdd9fdb08 | ||
|   | 0a6ccd8148 | ||
|   | e214ee617d | ||
|   | 4ffc671663 | ||
|   | 99f7d73eb9 | ||
|   | a21164108c | ||
|   | de10d9b232 | ||
|   | 4472815435 | ||
|   | 2cc1339a4b | ||
|   | 77fabf5c92 | ||
|   | c7b1b62dc2 | ||
|   | a9414ae42d | ||
|   | de3cfd4f50 | ||
|   | 7b3d276780 | ||
|   | 21c08897b6 | ||
|   | 7228c4eeb6 | ||
|   | 37b972763b | ||
|   | 911f7c36a3 | ||
|   | 4fd78a0b02 | ||
|   | f61ef4090c | ||
|   | 1d86424718 | ||
|   | fc9288ac51 | ||
|   | 9fef4db20b | ||
|   | dce7e4779d | ||
|   | 6731a741fa | ||
|   | 9667866982 | ||
|   | 4dacb46a8e | ||
|   | 5f7f88421e | ||
|   | bc941cb248 | ||
|   | 00d15de44f | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a4f17de0d1 | ||
|   | 6867667de6 | ||
|   | 079886b54c | ||
|   | 19eb8f3064 | ||
|   | 1a13936693 | ||
|   | af26d705cd | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 2d4ceda12f | ||
|   | 4a87af4463 | ||
|   | 88d918d608 | ||
|   | 5854d973ea | ||
|   | f25ab72747 | ||
|   | 2233557990 | ||
|   | d3bec635f8 | ||
|   | 6519644fc1 | ||
|   | 736f65b7b2 | ||
|   | 63d39b5500 | ||
|   | b735ab6f39 | ||
|   | 232c50eaef | ||
|   | 52b12abeb2 | ||
|   | 48b4d78a7c | ||
|   | 8ebed0ac9a | ||
|   | e742603c15 | ||
|   | 3215bb6baa | ||
|   | a11aba72d8 | ||
|   | 10d1b48505 | ||
|   | f73eb9571f | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | da2877a682 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 33cbfef02a | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | c1a6428ed3 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 2ee7932cba | ||
|   | fe440a6f2e | ||
|   | 0ace88a877 | ||
|   | 476ed6964d | ||
|   | b3dca0429f | ||
|   | 9e4b68112c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 364f0e221e | ||
|   | 09635666aa | ||
|   | 9f02710114 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 64bdab5e5b | ||
|   | 0f4a6b5924 | ||
|   | c662b9e222 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a4722db7d7 | ||
|   | f48bb65d7b | ||
|   | 7e604419ab | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 60cd0a216f | ||
|   | ec6e3aa718 | ||
|   | 6dc57ddf0f | ||
|   | f780e81ec2 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 8b70ab47a4 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | b800359bb2 | ||
|   | 6ec8c9766c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 4524e3322c | ||
|   | 1941de1125 | ||
|   | 49c4c7a455 | ||
|   | c10bff55de | ||
|   | 7640e956c2 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 4c18a4c44d | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 87a5c0f3f1 | ||
|   | 84d4c84ed2 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 9008b67f7d | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 47980a3dd0 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 6cbb9e93c0 | ||
|   | f3ec4baf3c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | b5799da703 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 80b1820333 | ||
|   | aed29d2923 | ||
|   | 3397e2aa8e | ||
|   | ee83c177f4 | 
							
								
								
									
										7
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -39,4 +39,9 @@ jobs: | |||||||
|           cp -r frontend/dist internal/assets/dist |           cp -r frontend/dist internal/assets/dist | ||||||
|  |  | ||||||
|       - name: Run tests |       - name: Run tests | ||||||
|         run: go test -v ./... |         run: go test -coverprofile=coverage.txt -v ./... | ||||||
|  |  | ||||||
|  |       - name: Upload coverage reports to Codecov | ||||||
|  |         uses: codecov/codecov-action@v5 | ||||||
|  |         with: | ||||||
|  |           token: ${{ secrets.CODECOV_TOKEN }} | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/nightly.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/nightly.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,6 +1,8 @@ | |||||||
| name: Nightly Release | name: Nightly Release | ||||||
| on: | on: | ||||||
|   workflow_dispatch: |   workflow_dispatch: | ||||||
|  |   schedule: | ||||||
|  |     - cron: "0 0 * * *" | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   create-release: |   create-release: | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -11,11 +11,7 @@ docker-compose.test* | |||||||
| users.txt | users.txt | ||||||
|  |  | ||||||
| # secret test file | # secret test file | ||||||
| secret.txt | secret* | ||||||
| secret_oauth.txt |  | ||||||
|  |  | ||||||
| # vscode |  | ||||||
| .vscode |  | ||||||
|  |  | ||||||
| # apple stuff | # apple stuff | ||||||
| .DS_Store | .DS_Store | ||||||
|   | |||||||
							
								
								
									
										15
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | { | ||||||
|  |   "version": "0.2.0", | ||||||
|  |   "configurations": [ | ||||||
|  |     { | ||||||
|  |       "name": "Connect to server", | ||||||
|  |       "type": "go", | ||||||
|  |       "request": "attach", | ||||||
|  |       "mode": "remote", | ||||||
|  |       "remotePath": "/tinyauth", | ||||||
|  |       "port": 4000, | ||||||
|  |       "host": "127.0.0.1", | ||||||
|  |       "debugAdapter": "legacy" | ||||||
|  |     } | ||||||
|  |   ] | ||||||
|  | } | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| # Site builder | # Site builder | ||||||
| FROM oven/bun:1.2.15-alpine AS frontend-builder | FROM oven/bun:1.2.18-alpine AS frontend-builder | ||||||
|  |  | ||||||
| WORKDIR /frontend | WORKDIR /frontend | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										16
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								README.md
									
									
									
									
									
								
							| @@ -23,27 +23,27 @@ Tinyauth is a simple authentication middleware that adds a simple login screen o | |||||||
|  |  | ||||||
| ## Getting Started | ## 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.html). There is also an available [docker compose](./docker-compose.example.yml) file that has Traefik, Whoami and Tinyauth to demonstrate its capabilities. | ||||||
|  |  | ||||||
| ## Demo | ## Demo | ||||||
|  |  | ||||||
| If you are still not sure if tinyauth suits your needs you can try out the [demo](https://demo.tinyauth.app). The default username is `user` and the default password is `password`. | If you are still not sure if Tinyauth suits your needs you can try out the [demo](https://demo.tinyauth.app). The default username is `user` and the default password is `password`. | ||||||
|  |  | ||||||
| ## Documentation | ## Documentation | ||||||
|  |  | ||||||
| You can find documentation and guides on all of the available configuration of tinyauth in the [website](https://tinyauth.app). | You can find documentation and guides on all of the available configuration of Tinyauth in the [website](https://tinyauth.app). | ||||||
|  |  | ||||||
| ## Discord | ## Discord | ||||||
|  |  | ||||||
| Tinyauth has a [discord](https://discord.gg/eHzVaCzRRd) server. Feel free to hop in to chat about self-hosting, homelabs and of course tinyauth. See you there! | Tinyauth has a [discord](https://discord.gg/eHzVaCzRRd) server. Feel free to hop in to chat about self-hosting, homelabs and of course Tinyauth. See you there! | ||||||
|  |  | ||||||
| ## Contributing | ## Contributing | ||||||
|  |  | ||||||
| All contributions to the codebase are welcome! If you have any free time feel free to pick up an [Issue](https://github.com/steveiliop56/tinyauth/issues) or add your own missing features. Make sure to check out the [contributing guide](./CONTRIBUTING.md) for instructions on how to get the development server up and running. | All contributions to the codebase are welcome! If you have any free time feel free to pick up an [issue](https://github.com/steveiliop56/tinyauth/issues) or add your own missing features. Make sure to check out the [contributing guide](./CONTRIBUTING.md) for instructions on how to get the development server up and running. | ||||||
|  |  | ||||||
| ## Localization | ## Localization | ||||||
|  |  | ||||||
| If you would like to help translate tinyauth into more languages, visit the [Crowdin](https://crowdin.com/project/tinyauth) page. | If you would like to help translate Tinyauth into more languages, visit the [Crowdin](https://crowdin.com/project/tinyauth) page. | ||||||
|  |  | ||||||
| ## License | ## License | ||||||
|  |  | ||||||
| @@ -51,9 +51,9 @@ Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You ma | |||||||
|  |  | ||||||
| ## Sponsors | ## Sponsors | ||||||
|  |  | ||||||
| Thanks a lot to the following people for providing me with more coffee: | A big thank you to the following people for providing me with more coffee: | ||||||
|  |  | ||||||
| <!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https://github.com/erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a>  <a href="https://github.com/nicotsx"><img src="https://github.com/nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a>  <a href="https://github.com/SimpleHomelab"><img src="https://github.com/SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a>  <a href="https://github.com/jmadden91"><img src="https://github.com/jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a>  <a href="https://github.com/tribor"><img src="https://github.com/tribor.png" width="64px" alt="User avatar: tribor" /></a>  <a href="https://github.com/eliasbenb"><img src="https://github.com/eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a>  <!-- sponsors --> | <!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https://github.com/erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a>  <a href="https://github.com/nicotsx"><img src="https://github.com/nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a>  <a href="https://github.com/SimpleHomelab"><img src="https://github.com/SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a>  <a href="https://github.com/jmadden91"><img src="https://github.com/jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a>  <a href="https://github.com/tribor"><img src="https://github.com/tribor.png" width="64px" alt="User avatar: tribor" /></a>  <a href="https://github.com/eliasbenb"><img src="https://github.com/eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a>  <a href="https://github.com/afunworm"><img src="https://github.com/afunworm.png" width="64px" alt="User avatar: afunworm" /></a>  <!-- sponsors --> | ||||||
|  |  | ||||||
| ## Acknowledgements | ## Acknowledgements | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,8 +2,8 @@ | |||||||
|  |  | ||||||
| ## Supported Versions | ## Supported Versions | ||||||
|  |  | ||||||
| Please always use the latest available Tinyauth version which can be found [here](https://github.com/steveiliop56/tinyauth/releases/latest). Older versions (especially major) may contain security issues which I cannot go back and fix. | It is recommended to use the [latest](https://github.com/steveiliop56/tinyauth/releases/latest) available version of tinyauth. This is because it includes security fixes, new features and dependency updates. Older versions, especially major ones, are not supported and won't receive security or patch updates. | ||||||
|  |  | ||||||
| ## Reporting a Vulnerability | ## Reporting a Vulnerability | ||||||
|  |  | ||||||
| Due to the nature of this app, it needs to be secure. If you find any security issues in the OAuth or login flow of the app please contact me at <steve@doesmycode.work> and include a concise description of the issue. Please do not use the issues section for reporting major security issues. | Due to the nature of this app, it needs to be secure. If you discover any security issues or vulnerabilities in the app please contact me as soon as possible at <steve@doesmycode.work>. Please do not use the issues section to report security issues as I won't be able to patch them in time and they may get exploited by malicious actors. | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								air.toml
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								air.toml
									
									
									
									
									
								
							| @@ -2,9 +2,9 @@ root = "/tinyauth" | |||||||
| tmp_dir = "tmp" | tmp_dir = "tmp" | ||||||
|  |  | ||||||
| [build] | [build] | ||||||
| pre_cmd = ["mkdir -p internal/assets/dist", "echo 'backend running' > internal/assets/dist/index.html"] | 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 -o ./tmp/tinyauth ." | cmd = "CGO_ENABLED=0 go build -gcflags=\"all=-N -l\" -o tmp/tinyauth ." | ||||||
| bin = "tmp/tinyauth" | bin = "/go/bin/dlv --listen :4000 --headless=true --api-version=2 --accept-multiclient --log=true exec tmp/tinyauth --continue" | ||||||
| include_ext = ["go"] | include_ext = ["go"] | ||||||
| exclude_dir = ["internal/assets/dist"] | exclude_dir = ["internal/assets/dist"] | ||||||
| exclude_regex = [".*_test\\.go"] | exclude_regex = [".*_test\\.go"] | ||||||
|   | |||||||
							
								
								
									
										119
									
								
								cmd/root.go
									
									
									
									
									
								
							
							
						
						
									
										119
									
								
								cmd/root.go
									
									
									
									
									
								
							| @@ -3,18 +3,17 @@ package cmd | |||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" |  | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" |  | ||||||
| 	totpCmd "tinyauth/cmd/totp" | 	totpCmd "tinyauth/cmd/totp" | ||||||
| 	userCmd "tinyauth/cmd/user" | 	userCmd "tinyauth/cmd/user" | ||||||
| 	"tinyauth/internal/api" |  | ||||||
| 	"tinyauth/internal/auth" | 	"tinyauth/internal/auth" | ||||||
| 	"tinyauth/internal/constants" | 	"tinyauth/internal/constants" | ||||||
| 	"tinyauth/internal/docker" | 	"tinyauth/internal/docker" | ||||||
| 	"tinyauth/internal/handlers" | 	"tinyauth/internal/handlers" | ||||||
| 	"tinyauth/internal/hooks" | 	"tinyauth/internal/hooks" | ||||||
|  | 	"tinyauth/internal/ldap" | ||||||
| 	"tinyauth/internal/providers" | 	"tinyauth/internal/providers" | ||||||
|  | 	"tinyauth/internal/server" | ||||||
| 	"tinyauth/internal/types" | 	"tinyauth/internal/types" | ||||||
| 	"tinyauth/internal/utils" | 	"tinyauth/internal/utils" | ||||||
|  |  | ||||||
| @@ -30,51 +29,46 @@ var rootCmd = &cobra.Command{ | |||||||
| 	Short: "The simplest way to protect your apps with a login screen.", | 	Short: "The simplest way to protect your apps with a login screen.", | ||||||
| 	Long:  `Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic OAuth provider to all of your docker apps.`, | 	Long:  `Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic OAuth provider to all of your docker apps.`, | ||||||
| 	Run: func(cmd *cobra.Command, args []string) { | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 		// Logger |  | ||||||
| 		log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Logger().Level(zerolog.FatalLevel) |  | ||||||
|  |  | ||||||
| 		// Get config |  | ||||||
| 		var config types.Config | 		var config types.Config | ||||||
| 		err := viper.Unmarshal(&config) | 		err := viper.Unmarshal(&config) | ||||||
| 		HandleError(err, "Failed to parse config") | 		HandleError(err, "Failed to parse config") | ||||||
|  |  | ||||||
| 		// Secrets | 		// Check if secrets have a file associated with them | ||||||
| 		config.Secret = utils.GetSecret(config.Secret, config.SecretFile) | 		config.Secret = utils.GetSecret(config.Secret, config.SecretFile) | ||||||
| 		config.GithubClientSecret = utils.GetSecret(config.GithubClientSecret, config.GithubClientSecretFile) | 		config.GithubClientSecret = utils.GetSecret(config.GithubClientSecret, config.GithubClientSecretFile) | ||||||
| 		config.GoogleClientSecret = utils.GetSecret(config.GoogleClientSecret, config.GoogleClientSecretFile) | 		config.GoogleClientSecret = utils.GetSecret(config.GoogleClientSecret, config.GoogleClientSecretFile) | ||||||
| 		config.GenericClientSecret = utils.GetSecret(config.GenericClientSecret, config.GenericClientSecretFile) | 		config.GenericClientSecret = utils.GetSecret(config.GenericClientSecret, config.GenericClientSecretFile) | ||||||
|  |  | ||||||
| 		// Validate config |  | ||||||
| 		validator := validator.New() | 		validator := validator.New() | ||||||
| 		err = validator.Struct(config) | 		err = validator.Struct(config) | ||||||
| 		HandleError(err, "Failed to validate config") | 		HandleError(err, "Failed to validate config") | ||||||
|  |  | ||||||
| 		// Logger |  | ||||||
| 		log.Logger = log.Level(zerolog.Level(config.LogLevel)) | 		log.Logger = log.Level(zerolog.Level(config.LogLevel)) | ||||||
| 		log.Info().Str("version", strings.TrimSpace(constants.Version)).Msg("Starting tinyauth") | 		log.Info().Str("version", strings.TrimSpace(constants.Version)).Msg("Starting tinyauth") | ||||||
|  |  | ||||||
| 		// Users |  | ||||||
| 		log.Info().Msg("Parsing users") | 		log.Info().Msg("Parsing users") | ||||||
| 		users, err := utils.GetUsers(config.Users, config.UsersFile) | 		users, err := utils.GetUsers(config.Users, config.UsersFile) | ||||||
| 		HandleError(err, "Failed to parse users") | 		HandleError(err, "Failed to parse users") | ||||||
|  |  | ||||||
| 		if len(users) == 0 && !utils.OAuthConfigured(config) { |  | ||||||
| 			HandleError(errors.New("no users or OAuth configured"), "No users or OAuth configured") |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// Get domain |  | ||||||
| 		log.Debug().Msg("Getting domain") | 		log.Debug().Msg("Getting domain") | ||||||
| 		domain, err := utils.GetUpperDomain(config.AppURL) | 		domain, err := utils.GetUpperDomain(config.AppURL) | ||||||
| 		HandleError(err, "Failed to get upper domain") | 		HandleError(err, "Failed to get upper domain") | ||||||
| 		log.Info().Str("domain", domain).Msg("Using domain for cookie store") | 		log.Info().Str("domain", domain).Msg("Using domain for cookie store") | ||||||
|  |  | ||||||
| 		// Generate cookie name |  | ||||||
| 		cookieId := utils.GenerateIdentifier(strings.Split(domain, ".")[0]) | 		cookieId := utils.GenerateIdentifier(strings.Split(domain, ".")[0]) | ||||||
| 		sessionCookieName := fmt.Sprintf("%s-%s", constants.SessionCookieName, cookieId) | 		sessionCookieName := fmt.Sprintf("%s-%s", constants.SessionCookieName, cookieId) | ||||||
| 		csrfCookieName := fmt.Sprintf("%s-%s", constants.CsrfCookieName, cookieId) | 		csrfCookieName := fmt.Sprintf("%s-%s", constants.CsrfCookieName, cookieId) | ||||||
| 		redirectCookieName := fmt.Sprintf("%s-%s", constants.RedirectCookieName, cookieId) | 		redirectCookieName := fmt.Sprintf("%s-%s", constants.RedirectCookieName, cookieId) | ||||||
|  |  | ||||||
| 		// Create OAuth config | 		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{ | 		oauthConfig := types.OAuthConfig{ | ||||||
| 			GithubClientId:      config.GithubClientId, | 			GithubClientId:      config.GithubClientId, | ||||||
| 			GithubClientSecret:  config.GithubClientSecret, | 			GithubClientSecret:  config.GithubClientSecret, | ||||||
| @@ -90,7 +84,6 @@ var rootCmd = &cobra.Command{ | |||||||
| 			AppURL:              config.AppURL, | 			AppURL:              config.AppURL, | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Create handlers config |  | ||||||
| 		handlersConfig := types.HandlersConfig{ | 		handlersConfig := types.HandlersConfig{ | ||||||
| 			AppURL:                config.AppURL, | 			AppURL:                config.AppURL, | ||||||
| 			DisableContinue:       config.DisableContinue, | 			DisableContinue:       config.DisableContinue, | ||||||
| @@ -105,61 +98,67 @@ var rootCmd = &cobra.Command{ | |||||||
| 			RedirectCookieName:    redirectCookieName, | 			RedirectCookieName:    redirectCookieName, | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Create api config | 		serverConfig := types.ServerConfig{ | ||||||
| 		apiConfig := types.APIConfig{ |  | ||||||
| 			Port:    config.Port, | 			Port:    config.Port, | ||||||
| 			Address: config.Address, | 			Address: config.Address, | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Create auth config |  | ||||||
| 		authConfig := types.AuthConfig{ | 		authConfig := types.AuthConfig{ | ||||||
| 			Users:             users, | 			Users:             users, | ||||||
| 			OauthWhitelist:    config.OAuthWhitelist, | 			OauthWhitelist:    config.OAuthWhitelist, | ||||||
| 			Secret:            config.Secret, |  | ||||||
| 			CookieSecure:      config.CookieSecure, | 			CookieSecure:      config.CookieSecure, | ||||||
| 			SessionExpiry:     config.SessionExpiry, | 			SessionExpiry:     config.SessionExpiry, | ||||||
| 			Domain:            domain, | 			Domain:            domain, | ||||||
| 			LoginTimeout:      config.LoginTimeout, | 			LoginTimeout:      config.LoginTimeout, | ||||||
| 			LoginMaxRetries:   config.LoginMaxRetries, | 			LoginMaxRetries:   config.LoginMaxRetries, | ||||||
| 			SessionCookieName: sessionCookieName, | 			SessionCookieName: sessionCookieName, | ||||||
|  | 			HMACSecret:        hmacSecret, | ||||||
|  | 			EncryptionSecret:  encryptionSecret, | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Create hooks config |  | ||||||
| 		hooksConfig := types.HooksConfig{ | 		hooksConfig := types.HooksConfig{ | ||||||
| 			Domain: domain, | 			Domain: domain, | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Create docker service | 		var ldapService *ldap.LDAP | ||||||
| 		docker := docker.NewDocker() |  | ||||||
|  |  | ||||||
| 		// Initialize docker | 		if config.LdapAddress != "" { | ||||||
| 		err = docker.Init() | 			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") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// 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") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Setup the services | ||||||
|  | 		docker, err := docker.NewDocker() | ||||||
| 		HandleError(err, "Failed to initialize docker") | 		HandleError(err, "Failed to initialize docker") | ||||||
|  | 		auth := auth.NewAuth(authConfig, docker, ldapService) | ||||||
| 		// Create auth service |  | ||||||
| 		auth := auth.NewAuth(authConfig, docker) |  | ||||||
|  |  | ||||||
| 		// Create OAuth providers service |  | ||||||
| 		providers := providers.NewProviders(oauthConfig) | 		providers := providers.NewProviders(oauthConfig) | ||||||
|  |  | ||||||
| 		// Initialize providers |  | ||||||
| 		providers.Init() |  | ||||||
|  |  | ||||||
| 		// Create hooks service |  | ||||||
| 		hooks := hooks.NewHooks(hooksConfig, auth, providers) | 		hooks := hooks.NewHooks(hooksConfig, auth, providers) | ||||||
|  |  | ||||||
| 		// Create handlers |  | ||||||
| 		handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker) | 		handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker) | ||||||
|  | 		srv, err := server.NewServer(serverConfig, handlers) | ||||||
|  | 		HandleError(err, "Failed to create server") | ||||||
|  |  | ||||||
| 		// Create API | 		// Start up | ||||||
| 		api := api.NewAPI(apiConfig, handlers) | 		err = srv.Start() | ||||||
|  | 		HandleError(err, "Failed to start server") | ||||||
| 		// Setup routes |  | ||||||
| 		api.Init() |  | ||||||
| 		api.SetupRoutes() |  | ||||||
|  |  | ||||||
| 		// Start |  | ||||||
| 		api.Run() |  | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -169,23 +168,17 @@ func Execute() { | |||||||
| } | } | ||||||
|  |  | ||||||
| func HandleError(err error, msg string) { | func HandleError(err error, msg string) { | ||||||
| 	// If error, log it and exit |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal().Err(err).Msg(msg) | 		log.Fatal().Err(err).Msg(msg) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func init() { | func init() { | ||||||
| 	// Add user command |  | ||||||
| 	rootCmd.AddCommand(userCmd.UserCmd()) | 	rootCmd.AddCommand(userCmd.UserCmd()) | ||||||
|  |  | ||||||
| 	// Add totp command |  | ||||||
| 	rootCmd.AddCommand(totpCmd.TotpCmd()) | 	rootCmd.AddCommand(totpCmd.TotpCmd()) | ||||||
|  |  | ||||||
| 	// Read environment variables |  | ||||||
| 	viper.AutomaticEnv() | 	viper.AutomaticEnv() | ||||||
|  |  | ||||||
| 	// Flags |  | ||||||
| 	rootCmd.Flags().Int("port", 3000, "Port to run the server on.") | 	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("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", "", "Secret to use for the cookie.") | ||||||
| @@ -217,10 +210,15 @@ func init() { | |||||||
| 	rootCmd.Flags().Int("login-max-retries", 5, "Maximum login attempts before timeout (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().Int("log-level", 1, "Log level.") | ||||||
| 	rootCmd.Flags().String("app-title", "Tinyauth", "Title of the app.") | 	rootCmd.Flags().String("app-title", "Tinyauth", "Title of the app.") | ||||||
| 	rootCmd.Flags().String("forgot-password-message", "You can reset your password by changing the `USERS` environment variable.", "Message to show on the forgot password page.") | 	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("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.") | ||||||
|  |  | ||||||
| 	// Bind flags to environment |  | ||||||
| 	viper.BindEnv("port", "PORT") | 	viper.BindEnv("port", "PORT") | ||||||
| 	viper.BindEnv("address", "ADDRESS") | 	viper.BindEnv("address", "ADDRESS") | ||||||
| 	viper.BindEnv("secret", "SECRET") | 	viper.BindEnv("secret", "SECRET") | ||||||
| @@ -254,7 +252,12 @@ func init() { | |||||||
| 	viper.BindEnv("login-max-retries", "LOGIN_MAX_RETRIES") | 	viper.BindEnv("login-max-retries", "LOGIN_MAX_RETRIES") | ||||||
| 	viper.BindEnv("forgot-password-message", "FORGOT_PASSWORD_MESSAGE") | 	viper.BindEnv("forgot-password-message", "FORGOT_PASSWORD_MESSAGE") | ||||||
| 	viper.BindEnv("background-image", "BACKGROUND_IMAGE") | 	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") | ||||||
|  |  | ||||||
| 	// Bind flags to viper |  | ||||||
| 	viper.BindPFlags(rootCmd.Flags()) | 	viper.BindPFlags(rootCmd.Flags()) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -15,7 +15,6 @@ import ( | |||||||
| 	"github.com/spf13/cobra" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Interactive flag |  | ||||||
| var interactive bool | var interactive bool | ||||||
|  |  | ||||||
| // Input user | // Input user | ||||||
| @@ -25,15 +24,9 @@ var GenerateCmd = &cobra.Command{ | |||||||
| 	Use:   "generate", | 	Use:   "generate", | ||||||
| 	Short: "Generate a totp secret", | 	Short: "Generate a totp secret", | ||||||
| 	Run: func(cmd *cobra.Command, args []string) { | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 		// Setup logger |  | ||||||
| 		log.Logger = log.Level(zerolog.InfoLevel) | 		log.Logger = log.Level(zerolog.InfoLevel) | ||||||
|  |  | ||||||
| 		// Use simple theme |  | ||||||
| 		var baseTheme *huh.Theme = huh.ThemeBase() |  | ||||||
|  |  | ||||||
| 		// Interactive |  | ||||||
| 		if interactive { | 		if interactive { | ||||||
| 			// Create huh form |  | ||||||
| 			form := huh.NewForm( | 			form := huh.NewForm( | ||||||
| 				huh.NewGroup( | 				huh.NewGroup( | ||||||
| 					huh.NewInput().Title("Current username:hash").Value(&iUser).Validate((func(s string) error { | 					huh.NewInput().Title("Current username:hash").Value(&iUser).Validate((func(s string) error { | ||||||
| @@ -44,51 +37,39 @@ var GenerateCmd = &cobra.Command{ | |||||||
| 					})), | 					})), | ||||||
| 				), | 				), | ||||||
| 			) | 			) | ||||||
|  | 			var baseTheme *huh.Theme = huh.ThemeBase() | ||||||
| 			// Run form |  | ||||||
| 			err := form.WithTheme(baseTheme).Run() | 			err := form.WithTheme(baseTheme).Run() | ||||||
|  |  | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				log.Fatal().Err(err).Msg("Form failed") | 				log.Fatal().Err(err).Msg("Form failed") | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Parse user |  | ||||||
| 		user, err := utils.ParseUser(iUser) | 		user, err := utils.ParseUser(iUser) | ||||||
|  |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Fatal().Err(err).Msg("Failed to parse user") | 			log.Fatal().Err(err).Msg("Failed to parse user") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Check if user was using docker escape |  | ||||||
| 		dockerEscape := false | 		dockerEscape := false | ||||||
|  |  | ||||||
| 		if strings.Contains(iUser, "$$") { | 		if strings.Contains(iUser, "$$") { | ||||||
| 			dockerEscape = true | 			dockerEscape = true | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Check it has totp |  | ||||||
| 		if user.TotpSecret != "" { | 		if user.TotpSecret != "" { | ||||||
| 			log.Fatal().Msg("User already has a totp secret") | 			log.Fatal().Msg("User already has a totp secret") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Generate totp secret |  | ||||||
| 		key, err := totp.Generate(totp.GenerateOpts{ | 		key, err := totp.Generate(totp.GenerateOpts{ | ||||||
| 			Issuer:      "Tinyauth", | 			Issuer:      "Tinyauth", | ||||||
| 			AccountName: user.Username, | 			AccountName: user.Username, | ||||||
| 		}) | 		}) | ||||||
|  |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Fatal().Err(err).Msg("Failed to generate totp secret") | 			log.Fatal().Err(err).Msg("Failed to generate totp secret") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Create secret |  | ||||||
| 		secret := key.Secret() | 		secret := key.Secret() | ||||||
|  |  | ||||||
| 		// Print secret and image |  | ||||||
| 		log.Info().Str("secret", secret).Msg("Generated totp secret") | 		log.Info().Str("secret", secret).Msg("Generated totp secret") | ||||||
|  |  | ||||||
| 		// Print QR code |  | ||||||
| 		log.Info().Msg("Generated QR code") | 		log.Info().Msg("Generated QR code") | ||||||
|  |  | ||||||
| 		config := qrterminal.Config{ | 		config := qrterminal.Config{ | ||||||
| @@ -101,7 +82,6 @@ var GenerateCmd = &cobra.Command{ | |||||||
|  |  | ||||||
| 		qrterminal.GenerateWithConfig(key.URL(), config) | 		qrterminal.GenerateWithConfig(key.URL(), config) | ||||||
|  |  | ||||||
| 		// Add the secret to the user |  | ||||||
| 		user.TotpSecret = secret | 		user.TotpSecret = secret | ||||||
|  |  | ||||||
| 		// If using docker escape re-escape it | 		// If using docker escape re-escape it | ||||||
| @@ -109,13 +89,11 @@ var GenerateCmd = &cobra.Command{ | |||||||
| 			user.Password = strings.ReplaceAll(user.Password, "$", "$$") | 			user.Password = strings.ReplaceAll(user.Password, "$", "$$") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Print success |  | ||||||
| 		log.Info().Str("user", fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TotpSecret)).Msg("Add the totp secret to your authenticator app then use the verify command to ensure everything is working correctly.") | 		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() { | func init() { | ||||||
| 	// Add interactive flag |  | ||||||
| 	GenerateCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Run in interactive mode") | 	GenerateCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Run in interactive mode") | ||||||
| 	GenerateCmd.Flags().StringVar(&iUser, "user", "", "Your current username:hash") | 	GenerateCmd.Flags().StringVar(&iUser, "user", "", "Your current username:hash") | ||||||
| } | } | ||||||
|   | |||||||
| @@ -7,16 +7,11 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| func TotpCmd() *cobra.Command { | func TotpCmd() *cobra.Command { | ||||||
| 	// Create the totp command |  | ||||||
| 	totpCmd := &cobra.Command{ | 	totpCmd := &cobra.Command{ | ||||||
| 		Use:   "totp", | 		Use:   "totp", | ||||||
| 		Short: "Totp utilities", | 		Short: "Totp utilities", | ||||||
| 		Long:  `Utilities for creating and verifying totp codes.`, | 		Long:  `Utilities for creating and verifying totp codes.`, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Add the generate command |  | ||||||
| 	totpCmd.AddCommand(generate.GenerateCmd) | 	totpCmd.AddCommand(generate.GenerateCmd) | ||||||
|  |  | ||||||
| 	// Return the totp command |  | ||||||
| 	return totpCmd | 	return totpCmd | ||||||
| } | } | ||||||
|   | |||||||
| @@ -12,10 +12,7 @@ import ( | |||||||
| 	"golang.org/x/crypto/bcrypt" | 	"golang.org/x/crypto/bcrypt" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Interactive flag |  | ||||||
| var interactive bool | var interactive bool | ||||||
|  |  | ||||||
| // Docker flag |  | ||||||
| var docker bool | var docker bool | ||||||
|  |  | ||||||
| // i stands for input | // i stands for input | ||||||
| @@ -27,12 +24,9 @@ var CreateCmd = &cobra.Command{ | |||||||
| 	Short: "Create a user", | 	Short: "Create a user", | ||||||
| 	Long:  `Create a user either interactively or by passing flags.`, | 	Long:  `Create a user either interactively or by passing flags.`, | ||||||
| 	Run: func(cmd *cobra.Command, args []string) { | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 		// Setup logger |  | ||||||
| 		log.Logger = log.Level(zerolog.InfoLevel) | 		log.Logger = log.Level(zerolog.InfoLevel) | ||||||
|  |  | ||||||
| 		// Check if interactive |  | ||||||
| 		if interactive { | 		if interactive { | ||||||
| 			// Create huh form |  | ||||||
| 			form := huh.NewForm( | 			form := huh.NewForm( | ||||||
| 				huh.NewGroup( | 				huh.NewGroup( | ||||||
| 					huh.NewInput().Title("Username").Value(&iUsername).Validate((func(s string) error { | 					huh.NewInput().Title("Username").Value(&iUsername).Validate((func(s string) error { | ||||||
| @@ -50,46 +44,35 @@ var CreateCmd = &cobra.Command{ | |||||||
| 					huh.NewSelect[bool]().Title("Format the output for docker?").Options(huh.NewOption("Yes", true), huh.NewOption("No", false)).Value(&docker), | 					huh.NewSelect[bool]().Title("Format the output for docker?").Options(huh.NewOption("Yes", true), huh.NewOption("No", false)).Value(&docker), | ||||||
| 				), | 				), | ||||||
| 			) | 			) | ||||||
|  |  | ||||||
| 			// Use simple theme |  | ||||||
| 			var baseTheme *huh.Theme = huh.ThemeBase() | 			var baseTheme *huh.Theme = huh.ThemeBase() | ||||||
|  |  | ||||||
| 			err := form.WithTheme(baseTheme).Run() | 			err := form.WithTheme(baseTheme).Run() | ||||||
|  |  | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				log.Fatal().Err(err).Msg("Form failed") | 				log.Fatal().Err(err).Msg("Form failed") | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Do we have username and password? |  | ||||||
| 		if iUsername == "" || iPassword == "" { | 		if iUsername == "" || iPassword == "" { | ||||||
| 			log.Fatal().Err(errors.New("error invalid input")).Msg("Username and password cannot be empty") | 			log.Fatal().Err(errors.New("error invalid input")).Msg("Username and password cannot be empty") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		log.Info().Str("username", iUsername).Str("password", iPassword).Bool("docker", docker).Msg("Creating user") | 		log.Info().Str("username", iUsername).Str("password", iPassword).Bool("docker", docker).Msg("Creating user") | ||||||
|  |  | ||||||
| 		// Hash password |  | ||||||
| 		password, err := bcrypt.GenerateFromPassword([]byte(iPassword), bcrypt.DefaultCost) | 		password, err := bcrypt.GenerateFromPassword([]byte(iPassword), bcrypt.DefaultCost) | ||||||
|  |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Fatal().Err(err).Msg("Failed to hash password") | 			log.Fatal().Err(err).Msg("Failed to hash password") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Convert password to string | 		// If docker format is enabled, escape the dollar sign | ||||||
| 		passwordString := string(password) | 		passwordString := string(password) | ||||||
|  |  | ||||||
| 		// Escape $ for docker |  | ||||||
| 		if docker { | 		if docker { | ||||||
| 			passwordString = strings.ReplaceAll(passwordString, "$", "$$") | 			passwordString = strings.ReplaceAll(passwordString, "$", "$$") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Log user created |  | ||||||
| 		log.Info().Str("user", fmt.Sprintf("%s:%s", iUsername, passwordString)).Msg("User created") | 		log.Info().Str("user", fmt.Sprintf("%s:%s", iUsername, passwordString)).Msg("User created") | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
| func init() { | func init() { | ||||||
| 	// Flags |  | ||||||
| 	CreateCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Create a user interactively") | 	CreateCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Create a user interactively") | ||||||
| 	CreateCmd.Flags().BoolVar(&docker, "docker", false, "Format output for docker") | 	CreateCmd.Flags().BoolVar(&docker, "docker", false, "Format output for docker") | ||||||
| 	CreateCmd.Flags().StringVar(&iUsername, "username", "", "Username") | 	CreateCmd.Flags().StringVar(&iUsername, "username", "", "Username") | ||||||
|   | |||||||
| @@ -8,17 +8,12 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| func UserCmd() *cobra.Command { | func UserCmd() *cobra.Command { | ||||||
| 	// Create the user command |  | ||||||
| 	userCmd := &cobra.Command{ | 	userCmd := &cobra.Command{ | ||||||
| 		Use:   "user", | 		Use:   "user", | ||||||
| 		Short: "User utilities", | 		Short: "User utilities", | ||||||
| 		Long:  `Utilities for creating and verifying tinyauth compatible users.`, | 		Long:  `Utilities for creating and verifying tinyauth compatible users.`, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Add subcommands |  | ||||||
| 	userCmd.AddCommand(create.CreateCmd) | 	userCmd.AddCommand(create.CreateCmd) | ||||||
| 	userCmd.AddCommand(verify.VerifyCmd) | 	userCmd.AddCommand(verify.VerifyCmd) | ||||||
|  |  | ||||||
| 	// Return the user command |  | ||||||
| 	return userCmd | 	return userCmd | ||||||
| } | } | ||||||
|   | |||||||
| @@ -12,10 +12,7 @@ import ( | |||||||
| 	"golang.org/x/crypto/bcrypt" | 	"golang.org/x/crypto/bcrypt" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Interactive flag |  | ||||||
| var interactive bool | var interactive bool | ||||||
|  |  | ||||||
| // Docker flag |  | ||||||
| var docker bool | var docker bool | ||||||
|  |  | ||||||
| // i stands for input | // i stands for input | ||||||
| @@ -29,15 +26,9 @@ var VerifyCmd = &cobra.Command{ | |||||||
| 	Short: "Verify a user is set up correctly", | 	Short: "Verify a user is set up correctly", | ||||||
| 	Long:  `Verify a user is set up correctly meaning that it has a correct username, password and totp code.`, | 	Long:  `Verify a user is set up correctly meaning that it has a correct username, password and totp code.`, | ||||||
| 	Run: func(cmd *cobra.Command, args []string) { | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 		// Setup logger |  | ||||||
| 		log.Logger = log.Level(zerolog.InfoLevel) | 		log.Logger = log.Level(zerolog.InfoLevel) | ||||||
|  |  | ||||||
| 		// Use simple theme |  | ||||||
| 		var baseTheme *huh.Theme = huh.ThemeBase() |  | ||||||
|  |  | ||||||
| 		// Check if interactive |  | ||||||
| 		if interactive { | 		if interactive { | ||||||
| 			// Create huh form |  | ||||||
| 			form := huh.NewForm( | 			form := huh.NewForm( | ||||||
| 				huh.NewGroup( | 				huh.NewGroup( | ||||||
| 					huh.NewInput().Title("User (username:hash:totp)").Value(&iUser).Validate((func(s string) error { | 					huh.NewInput().Title("User (username:hash:totp)").Value(&iUser).Validate((func(s string) error { | ||||||
| @@ -61,35 +52,27 @@ var VerifyCmd = &cobra.Command{ | |||||||
| 					huh.NewInput().Title("Totp Code (if setup)").Value(&iTotp), | 					huh.NewInput().Title("Totp Code (if setup)").Value(&iTotp), | ||||||
| 				), | 				), | ||||||
| 			) | 			) | ||||||
|  | 			var baseTheme *huh.Theme = huh.ThemeBase() | ||||||
| 			// Run form |  | ||||||
| 			err := form.WithTheme(baseTheme).Run() | 			err := form.WithTheme(baseTheme).Run() | ||||||
|  |  | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				log.Fatal().Err(err).Msg("Form failed") | 				log.Fatal().Err(err).Msg("Form failed") | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Parse user |  | ||||||
| 		user, err := utils.ParseUser(iUser) | 		user, err := utils.ParseUser(iUser) | ||||||
|  |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Fatal().Err(err).Msg("Failed to parse user") | 			log.Fatal().Err(err).Msg("Failed to parse user") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Compare username |  | ||||||
| 		if user.Username != iUsername { | 		if user.Username != iUsername { | ||||||
| 			log.Fatal().Msg("Username is incorrect") | 			log.Fatal().Msg("Username is incorrect") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Compare password |  | ||||||
| 		err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(iPassword)) | 		err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(iPassword)) | ||||||
|  |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Fatal().Msg("Ppassword is incorrect") | 			log.Fatal().Msg("Ppassword is incorrect") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Check if user has 2fa code |  | ||||||
| 		if user.TotpSecret == "" { | 		if user.TotpSecret == "" { | ||||||
| 			if iTotp != "" { | 			if iTotp != "" { | ||||||
| 				log.Warn().Msg("User does not have 2fa secret") | 				log.Warn().Msg("User does not have 2fa secret") | ||||||
| @@ -98,21 +81,17 @@ var VerifyCmd = &cobra.Command{ | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Check totp code |  | ||||||
| 		ok := totp.Validate(iTotp, user.TotpSecret) | 		ok := totp.Validate(iTotp, user.TotpSecret) | ||||||
|  |  | ||||||
| 		if !ok { | 		if !ok { | ||||||
| 			log.Fatal().Msg("Totp code incorrect") | 			log.Fatal().Msg("Totp code incorrect") | ||||||
|  |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Done |  | ||||||
| 		log.Info().Msg("User verified") | 		log.Info().Msg("User verified") | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
| func init() { | func init() { | ||||||
| 	// Flags |  | ||||||
| 	VerifyCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Create a user interactively") | 	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().BoolVar(&docker, "docker", false, "Is the user formatted for docker?") | ||||||
| 	VerifyCmd.Flags().StringVar(&iUsername, "username", "", "Username") | 	VerifyCmd.Flags().StringVar(&iUsername, "username", "", "Username") | ||||||
|   | |||||||
| @@ -7,7 +7,6 @@ import ( | |||||||
| 	"github.com/spf13/cobra" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Create the version command |  | ||||||
| var versionCmd = &cobra.Command{ | var versionCmd = &cobra.Command{ | ||||||
| 	Use:   "version", | 	Use:   "version", | ||||||
| 	Short: "Print the version number of Tinyauth", | 	Short: "Print the version number of Tinyauth", | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								codecov.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								codecov.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | coverage: | ||||||
|  |   status: | ||||||
|  |     project: | ||||||
|  |       default: | ||||||
|  |         informational: true | ||||||
|  |     patch: | ||||||
|  |       default: | ||||||
|  |         informational: true | ||||||
| @@ -42,6 +42,7 @@ services: | |||||||
|       - /var/run/docker.sock:/var/run/docker.sock |       - /var/run/docker.sock:/var/run/docker.sock | ||||||
|     ports: |     ports: | ||||||
|       - 3000:3000 |       - 3000:3000 | ||||||
|  |       - 4000:4000 | ||||||
|     labels: |     labels: | ||||||
|       traefik.enable: true |       traefik.enable: true | ||||||
|       traefik.http.middlewares.tinyauth.forwardauth.address: http://tinyauth-backend:3000/api/auth/traefik |       traefik.http.middlewares.tinyauth.forwardauth.address: http://tinyauth-backend:3000/api/auth/traefik | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| FROM oven/bun:1.1.45-alpine | FROM oven/bun:1.2.16-alpine | ||||||
|  |  | ||||||
| WORKDIR /frontend | WORKDIR /frontend | ||||||
|  |  | ||||||
|   | |||||||
| @@ -9,45 +9,45 @@ | |||||||
|         "@radix-ui/react-select": "^2.2.5", |         "@radix-ui/react-select": "^2.2.5", | ||||||
|         "@radix-ui/react-separator": "^1.1.7", |         "@radix-ui/react-separator": "^1.1.7", | ||||||
|         "@radix-ui/react-slot": "^1.2.3", |         "@radix-ui/react-slot": "^1.2.3", | ||||||
|         "@tailwindcss/vite": "^4.1.8", |         "@tailwindcss/vite": "^4.1.11", | ||||||
|         "@tanstack/react-query": "^5.80.6", |         "@tanstack/react-query": "^5.83.0", | ||||||
|         "axios": "^1.9.0", |         "axios": "^1.10.0", | ||||||
|         "class-variance-authority": "^0.7.1", |         "class-variance-authority": "^0.7.1", | ||||||
|         "clsx": "^2.1.1", |         "clsx": "^2.1.1", | ||||||
|         "dompurify": "^3.2.6", |         "dompurify": "^3.2.6", | ||||||
|         "i18next": "^25.2.1", |         "i18next": "^25.3.2", | ||||||
|         "i18next-browser-languagedetector": "^8.0.5", |         "i18next-browser-languagedetector": "^8.2.0", | ||||||
|         "i18next-resources-to-backend": "^1.2.1", |         "i18next-resources-to-backend": "^1.2.1", | ||||||
|         "input-otp": "^1.4.2", |         "input-otp": "^1.4.2", | ||||||
|         "lucide-react": "^0.513.0", |         "lucide-react": "^0.525.0", | ||||||
|         "next-themes": "^0.4.6", |         "next-themes": "^0.4.6", | ||||||
|         "react": "^19.0.0", |         "react": "^19.0.0", | ||||||
|         "react-dom": "^19.0.0", |         "react-dom": "^19.0.0", | ||||||
|         "react-hook-form": "^7.57.0", |         "react-hook-form": "^7.60.0", | ||||||
|         "react-i18next": "^15.5.2", |         "react-i18next": "^15.6.0", | ||||||
|         "react-markdown": "^10.1.0", |         "react-markdown": "^10.1.0", | ||||||
|         "react-router": "^7.6.2", |         "react-router": "^7.7.0", | ||||||
|         "sonner": "^2.0.5", |         "sonner": "^2.0.6", | ||||||
|         "tailwind-merge": "^3.3.0", |         "tailwind-merge": "^3.3.1", | ||||||
|         "tailwindcss": "^4.1.8", |         "tailwindcss": "^4.1.11", | ||||||
|         "zod": "^3.25.57", |         "zod": "^4.0.5", | ||||||
|       }, |       }, | ||||||
|       "devDependencies": { |       "devDependencies": { | ||||||
|         "@eslint/js": "^9.28.0", |         "@eslint/js": "^9.31.0", | ||||||
|         "@tanstack/eslint-plugin-query": "^5.78.0", |         "@tanstack/eslint-plugin-query": "^5.81.2", | ||||||
|         "@types/node": "^22.15.29", |         "@types/node": "^24.0.14", | ||||||
|         "@types/react": "^19.1.7", |         "@types/react": "^19.1.8", | ||||||
|         "@types/react-dom": "^19.1.6", |         "@types/react-dom": "^19.1.6", | ||||||
|         "@vitejs/plugin-react": "^4.5.2", |         "@vitejs/plugin-react": "^4.6.0", | ||||||
|         "eslint": "^9.28.0", |         "eslint": "^9.31.0", | ||||||
|         "eslint-plugin-react-hooks": "^5.2.0", |         "eslint-plugin-react-hooks": "^5.2.0", | ||||||
|         "eslint-plugin-react-refresh": "^0.4.19", |         "eslint-plugin-react-refresh": "^0.4.19", | ||||||
|         "globals": "^16.2.0", |         "globals": "^16.3.0", | ||||||
|         "prettier": "3.5.3", |         "prettier": "3.6.2", | ||||||
|         "tw-animate-css": "^1.3.4", |         "tw-animate-css": "^1.3.5", | ||||||
|         "typescript": "~5.8.3", |         "typescript": "~5.8.3", | ||||||
|         "typescript-eslint": "^8.34.0", |         "typescript-eslint": "^8.37.0", | ||||||
|         "vite": "^6.3.1", |         "vite": "^7.0.5", | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
| @@ -84,7 +84,7 @@ | |||||||
|  |  | ||||||
|     "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], |     "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], | ||||||
|  |  | ||||||
|     "@babel/runtime": ["@babel/runtime@7.27.1", "", {}, "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog=="], |     "@babel/runtime": ["@babel/runtime@7.27.6", "", {}, "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q=="], | ||||||
|  |  | ||||||
|     "@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/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=="], | ||||||
|  |  | ||||||
| @@ -146,15 +146,15 @@ | |||||||
|  |  | ||||||
|     "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], |     "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], | ||||||
|  |  | ||||||
|     "@eslint/config-array": ["@eslint/config-array@0.20.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ=="], |     "@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.2.2", "", {}, "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg=="], |     "@eslint/config-helpers": ["@eslint/config-helpers@0.3.0", "", {}, "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw=="], | ||||||
|  |  | ||||||
|     "@eslint/core": ["@eslint/core@0.14.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg=="], |     "@eslint/core": ["@eslint/core@0.15.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA=="], | ||||||
|  |  | ||||||
|     "@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/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.28.0", "", {}, "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg=="], |     "@eslint/js": ["@eslint/js@9.31.0", "", {}, "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw=="], | ||||||
|  |  | ||||||
|     "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], |     "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], | ||||||
|  |  | ||||||
| @@ -252,7 +252,7 @@ | |||||||
|  |  | ||||||
|     "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], |     "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], | ||||||
|  |  | ||||||
|     "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.11", "", {}, "sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag=="], |     "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.19", "", {}, "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA=="], | ||||||
|  |  | ||||||
|     "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.40.2", "", { "os": "android", "cpu": "arm" }, "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg=="], |     "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.40.2", "", { "os": "android", "cpu": "arm" }, "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg=="], | ||||||
|  |  | ||||||
| @@ -296,41 +296,41 @@ | |||||||
|  |  | ||||||
|     "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], |     "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], | ||||||
|  |  | ||||||
|     "@tailwindcss/node": ["@tailwindcss/node@4.1.8", "", { "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.8" } }, "sha512-OWwBsbC9BFAJelmnNcrKuf+bka2ZxCE2A4Ft53Tkg4uoiE67r/PMEYwCsourC26E+kmxfwE0hVzMdxqeW+xu7Q=="], |     "@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/oxide": ["@tailwindcss/oxide@4.1.8", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.8", "@tailwindcss/oxide-darwin-arm64": "4.1.8", "@tailwindcss/oxide-darwin-x64": "4.1.8", "@tailwindcss/oxide-freebsd-x64": "4.1.8", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.8", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.8", "@tailwindcss/oxide-linux-arm64-musl": "4.1.8", "@tailwindcss/oxide-linux-x64-gnu": "4.1.8", "@tailwindcss/oxide-linux-x64-musl": "4.1.8", "@tailwindcss/oxide-wasm32-wasi": "4.1.8", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.8", "@tailwindcss/oxide-win32-x64-msvc": "4.1.8" } }, "sha512-d7qvv9PsM5N3VNKhwVUhpK6r4h9wtLkJ6lz9ZY9aeZgrUWk1Z8VPyqyDT9MZlem7GTGseRQHkeB1j3tC7W1P+A=="], |     "@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-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.8", "", { "os": "android", "cpu": "arm64" }, "sha512-Fbz7qni62uKYceWYvUjRqhGfZKwhZDQhlrJKGtnZfuNtHFqa8wmr+Wn74CTWERiW2hn3mN5gTpOoxWKk0jRxjg=="], |     "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.11", "", { "os": "android", "cpu": "arm64" }, "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg=="], | ||||||
|  |  | ||||||
|     "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RdRvedGsT0vwVVDztvyXhKpsU2ark/BjgG0huo4+2BluxdXo8NDgzl77qh0T1nUxmM11eXwR8jA39ibvSTbi7A=="], |     "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ=="], | ||||||
|  |  | ||||||
|     "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-t6PgxjEMLp5Ovf7uMb2OFmb3kqzVTPPakWpBIFzppk4JE4ix0yEtbtSjPbU8+PZETpaYMtXvss2Sdkx8Vs4XRw=="], |     "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw=="], | ||||||
|  |  | ||||||
|     "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-g8C8eGEyhHTqwPStSwZNSrOlyx0bhK/V/+zX0Y+n7DoRUzyS8eMbVshVOLJTDDC+Qn9IJnilYbIKzpB9n4aBsg=="], |     "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA=="], | ||||||
|  |  | ||||||
|     "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.8", "", { "os": "linux", "cpu": "arm" }, "sha512-Jmzr3FA4S2tHhaC6yCjac3rGf7hG9R6Gf2z9i9JFcuyy0u79HfQsh/thifbYTF2ic82KJovKKkIB6Z9TdNhCXQ=="], |     "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11", "", { "os": "linux", "cpu": "arm" }, "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg=="], | ||||||
|  |  | ||||||
|     "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-qq7jXtO1+UEtCmCeBBIRDrPFIVI4ilEQ97qgBGdwXAARrUqSn/L9fUrkb1XP/mvVtoVeR2bt/0L77xx53bPZ/Q=="], |     "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ=="], | ||||||
|  |  | ||||||
|     "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-O6b8QesPbJCRshsNApsOIpzKt3ztG35gfX9tEf4arD7mwNinsoCKxkj8TgEE0YRjmjtO3r9FlJnT/ENd9EVefQ=="], |     "@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-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-32iEXX/pXwikshNOGnERAFwFSfiltmijMIAbUhnNyjFr3tmWmMJWQKU2vNcFX0DACSXJ3ZWcSkzNbaKTdngH6g=="], |     "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg=="], | ||||||
|  |  | ||||||
|     "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-s+VSSD+TfZeMEsCaFaHTaY5YNj3Dri8rST09gMvYQKwPphacRG7wbuQ5ZJMIJXN/puxPcg/nU+ucvWguPpvBDg=="], |     "@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-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.8", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.10", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-CXBPVFkpDjM67sS1psWohZ6g/2/cd+cq56vPxK4JeawelxwK4YECgl9Y9TjkE2qfF+9/s1tHHJqrC4SS6cVvSg=="], |     "@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-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-7GmYk1n28teDHUjPlIx4Z6Z4hHEgvP5ZW2QS9ygnDAdI/myh3HTHjDqtSqgu1BpRoI4OiLx+fThAyA1JePoENA=="], |     "@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-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.8", "", { "os": "win32", "cpu": "x64" }, "sha512-fou+U20j+Jl0EHwK92spoWISON2OBnCazIc038Xj2TdweYV33ZRkS9nwqiUi2d/Wba5xg5UoHfvynnb/UB49cQ=="], |     "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.11", "", { "os": "win32", "cpu": "x64" }, "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg=="], | ||||||
|  |  | ||||||
|     "@tailwindcss/vite": ["@tailwindcss/vite@4.1.8", "", { "dependencies": { "@tailwindcss/node": "4.1.8", "@tailwindcss/oxide": "4.1.8", "tailwindcss": "4.1.8" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-CQ+I8yxNV5/6uGaJjiuymgw0kEQiNKRinYbZXPdx1fk5WgiyReG0VaUx/Xq6aVNSUNJFzxm6o8FNKS5aMaim5A=="], |     "@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=="], | ||||||
|  |  | ||||||
|     "@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.78.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.18.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-hYkhWr3UP0CkAsn/phBVR98UQawbw8CmTSgWtdgEBUjI60/GBaEIkpgi/Bp/2I8eIDK4+vdY7ac6jZx+GR+hEQ=="], |     "@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.81.2", "", { "dependencies": { "@typescript-eslint/utils": "^8.18.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-h4k6P6fm5VhKP5NkK+0TTVpGGyKQdx6tk7NYYG7J7PkSu7ClpLgBihw7yzK8N3n5zPaF3IMyErxfoNiXWH/3/A=="], | ||||||
|  |  | ||||||
|     "@tanstack/query-core": ["@tanstack/query-core@5.80.6", "", {}, "sha512-nl7YxT/TAU+VTf+e2zTkObGTyY8YZBMnbgeA1ee66lIVqzKlYursAII6z5t0e6rXgwUMJSV4dshBTNacNpZHbQ=="], |     "@tanstack/query-core": ["@tanstack/query-core@5.83.0", "", {}, "sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA=="], | ||||||
|  |  | ||||||
|     "@tanstack/react-query": ["@tanstack/react-query@5.80.6", "", { "dependencies": { "@tanstack/query-core": "5.80.6" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-izX+5CnkpON3NQGcEm3/d7LfFQNo9ZpFtX2QsINgCYK9LT2VCIdi8D3bMaMSNhrAJCznRoAkFic76uvLroALBw=="], |     "@tanstack/react-query": ["@tanstack/react-query@5.83.0", "", { "dependencies": { "@tanstack/query-core": "5.83.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ=="], | ||||||
|  |  | ||||||
|     "@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=="], |     "@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=="], | ||||||
|  |  | ||||||
| @@ -354,9 +354,9 @@ | |||||||
|  |  | ||||||
|     "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], |     "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], | ||||||
|  |  | ||||||
|     "@types/node": ["@types/node@22.15.29", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ=="], |     "@types/node": ["@types/node@24.0.14", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw=="], | ||||||
|  |  | ||||||
|     "@types/react": ["@types/react@19.1.7", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-BnsPLV43ddr05N71gaGzyZ5hzkCmGwhMvYc8zmvI8Ci1bRkkDSzDDVfAXfN2tk748OwI7ediiPX6PfT9p0QGVg=="], |     "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], | ||||||
|  |  | ||||||
|     "@types/react-dom": ["@types/react-dom@19.1.6", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw=="], |     "@types/react-dom": ["@types/react-dom@19.1.6", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw=="], | ||||||
|  |  | ||||||
| @@ -364,31 +364,31 @@ | |||||||
|  |  | ||||||
|     "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], |     "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.34.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.34.0", "@typescript-eslint/type-utils": "8.34.0", "@typescript-eslint/utils": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.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.34.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w=="], |     "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.37.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.37.0", "@typescript-eslint/type-utils": "8.37.0", "@typescript-eslint/utils": "8.37.0", "@typescript-eslint/visitor-keys": "8.37.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.37.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/parser": ["@typescript-eslint/parser@8.34.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.34.0", "@typescript-eslint/types": "8.34.0", "@typescript-eslint/typescript-estree": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA=="], |     "@typescript-eslint/parser": ["@typescript-eslint/parser@8.37.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.37.0", "@typescript-eslint/types": "8.37.0", "@typescript-eslint/typescript-estree": "8.37.0", "@typescript-eslint/visitor-keys": "8.37.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.34.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.34.0", "@typescript-eslint/types": "^8.34.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw=="], |     "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.37.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.37.0", "@typescript-eslint/types": "^8.37.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.32.0", "", { "dependencies": { "@typescript-eslint/types": "8.32.0", "@typescript-eslint/visitor-keys": "8.32.0" } }, "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ=="], |     "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.34.1", "", { "dependencies": { "@typescript-eslint/types": "8.34.1", "@typescript-eslint/visitor-keys": "8.34.1" } }, "sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.34.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA=="], |     "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.37.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.34.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.34.0", "@typescript-eslint/utils": "8.34.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg=="], |     "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.37.0", "", { "dependencies": { "@typescript-eslint/types": "8.37.0", "@typescript-eslint/typescript-estree": "8.37.0", "@typescript-eslint/utils": "8.37.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/types": ["@typescript-eslint/types@8.32.0", "", {}, "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA=="], |     "@typescript-eslint/types": ["@typescript-eslint/types@8.34.1", "", {}, "sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.32.0", "", { "dependencies": { "@typescript-eslint/types": "8.32.0", "@typescript-eslint/visitor-keys": "8.32.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-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ=="], |     "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.37.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.37.0", "@typescript-eslint/tsconfig-utils": "8.37.0", "@typescript-eslint/types": "8.37.0", "@typescript-eslint/visitor-keys": "8.37.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-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/utils": ["@typescript-eslint/utils@8.32.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.32.0", "@typescript-eslint/types": "8.32.0", "@typescript-eslint/typescript-estree": "8.32.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw=="], |     "@typescript-eslint/utils": ["@typescript-eslint/utils@8.34.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.34.1", "@typescript-eslint/types": "8.34.1", "@typescript-eslint/typescript-estree": "8.34.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.34.0", "", { "dependencies": { "@typescript-eslint/types": "8.34.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA=="], |     "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.37.0", "", { "dependencies": { "@typescript-eslint/types": "8.37.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w=="], | ||||||
|  |  | ||||||
|     "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], |     "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], | ||||||
|  |  | ||||||
|     "@vitejs/plugin-react": ["@vitejs/plugin-react@4.5.2", "", { "dependencies": { "@babel/core": "^7.27.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.11", "@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-beta.0" } }, "sha512-QNVT3/Lxx99nMQWJWF7K4N6apUEuT0KlZA3mx/mVaoGj3smm/8rc8ezz15J1pcbcjDK0V15rpHetVfya08r76Q=="], |     "@vitejs/plugin-react": ["@vitejs/plugin-react@4.6.0", "", { "dependencies": { "@babel/core": "^7.27.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.19", "@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-beta.0" } }, "sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ=="], | ||||||
|  |  | ||||||
|     "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], |     "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], | ||||||
|  |  | ||||||
|     "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], |     "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], | ||||||
|  |  | ||||||
| @@ -402,7 +402,7 @@ | |||||||
|  |  | ||||||
|     "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], |     "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], | ||||||
|  |  | ||||||
|     "axios": ["axios@1.9.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg=="], |     "axios": ["axios@1.10.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw=="], | ||||||
|  |  | ||||||
|     "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], |     "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], | ||||||
|  |  | ||||||
| @@ -494,17 +494,17 @@ | |||||||
|  |  | ||||||
|     "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], |     "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], | ||||||
|  |  | ||||||
|     "eslint": ["eslint@9.28.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.28.0", "@eslint/plugin-kit": "^0.3.1", "@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.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.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-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ=="], |     "eslint": ["eslint@9.31.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.31.0", "@eslint/plugin-kit": "^0.3.1", "@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-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ=="], | ||||||
|  |  | ||||||
|     "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@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.20", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA=="], | ||||||
|  |  | ||||||
|     "eslint-scope": ["eslint-scope@8.3.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ=="], |     "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], | ||||||
|  |  | ||||||
|     "eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], |     "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], | ||||||
|  |  | ||||||
|     "espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="], |     "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], | ||||||
|  |  | ||||||
|     "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], |     "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], | ||||||
|  |  | ||||||
| @@ -528,7 +528,7 @@ | |||||||
|  |  | ||||||
|     "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], |     "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], | ||||||
|  |  | ||||||
|     "fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="], |     "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], | ||||||
|  |  | ||||||
|     "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], |     "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], | ||||||
|  |  | ||||||
| @@ -558,7 +558,7 @@ | |||||||
|  |  | ||||||
|     "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], |     "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], | ||||||
|  |  | ||||||
|     "globals": ["globals@16.2.0", "", {}, "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg=="], |     "globals": ["globals@16.3.0", "", {}, "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ=="], | ||||||
|  |  | ||||||
|     "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], |     "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], | ||||||
|  |  | ||||||
| @@ -582,9 +582,9 @@ | |||||||
|  |  | ||||||
|     "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], |     "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], | ||||||
|  |  | ||||||
|     "i18next": ["i18next@25.2.1", "", { "dependencies": { "@babel/runtime": "^7.27.1" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-+UoXK5wh+VlE1Zy5p6MjcvctHXAhRwQKCxiJD8noKZzIXmnAX8gdHX5fLPA3MEVxEN4vbZkQFy8N0LyD9tUqPw=="], |     "i18next": ["i18next@25.3.2", "", { "dependencies": { "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-JSnbZDxRVbphc5jiptxr3o2zocy5dEqpVm9qCGdJwRNO+9saUJS0/u4LnM/13C23fUEWxAylPqKU/NpMV/IjqA=="], | ||||||
|  |  | ||||||
|     "i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.1.0", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-mHZxNx1Lq09xt5kCauZ/4bsXOEA2pfpwSoU11/QTJB+pD94iONFwp+ohqi///PwiFvjFOxe1akYCdHyFo1ng5Q=="], |     "i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.0", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g=="], | ||||||
|  |  | ||||||
|     "i18next-resources-to-backend": ["i18next-resources-to-backend@1.2.1", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw=="], |     "i18next-resources-to-backend": ["i18next-resources-to-backend@1.2.1", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw=="], | ||||||
|  |  | ||||||
| @@ -636,27 +636,27 @@ | |||||||
|  |  | ||||||
|     "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], |     "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], | ||||||
|  |  | ||||||
|     "lightningcss": ["lightningcss@1.29.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.29.2", "lightningcss-darwin-x64": "1.29.2", "lightningcss-freebsd-x64": "1.29.2", "lightningcss-linux-arm-gnueabihf": "1.29.2", "lightningcss-linux-arm64-gnu": "1.29.2", "lightningcss-linux-arm64-musl": "1.29.2", "lightningcss-linux-x64-gnu": "1.29.2", "lightningcss-linux-x64-musl": "1.29.2", "lightningcss-win32-arm64-msvc": "1.29.2", "lightningcss-win32-x64-msvc": "1.29.2" } }, "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA=="], |     "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], | ||||||
|  |  | ||||||
|     "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.29.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA=="], |     "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], | ||||||
|  |  | ||||||
|     "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.29.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w=="], |     "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="], | ||||||
|  |  | ||||||
|     "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.29.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg=="], |     "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="], | ||||||
|  |  | ||||||
|     "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.29.2", "", { "os": "linux", "cpu": "arm" }, "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg=="], |     "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="], | ||||||
|  |  | ||||||
|     "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.29.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ=="], |     "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="], | ||||||
|  |  | ||||||
|     "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.29.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ=="], |     "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="], | ||||||
|  |  | ||||||
|     "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.29.2", "", { "os": "linux", "cpu": "x64" }, "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg=="], |     "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="], | ||||||
|  |  | ||||||
|     "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.29.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w=="], |     "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="], | ||||||
|  |  | ||||||
|     "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.29.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw=="], |     "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="], | ||||||
|  |  | ||||||
|     "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.29.2", "", { "os": "win32", "cpu": "x64" }, "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA=="], |     "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="], | ||||||
|  |  | ||||||
|     "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], |     "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], | ||||||
|  |  | ||||||
| @@ -666,7 +666,7 @@ | |||||||
|  |  | ||||||
|     "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], |     "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], | ||||||
|  |  | ||||||
|     "lucide-react": ["lucide-react@0.513.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-CJZKq2g8Y8yN4Aq002GahSXbG2JpFv9kXwyiOAMvUBv7pxeOFHUWKB0mO7MiY4ZVFCV4aNjv2BJFq/z3DgKPQg=="], |     "lucide-react": ["lucide-react@0.525.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ=="], | ||||||
|  |  | ||||||
|     "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.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], | ||||||
|  |  | ||||||
| @@ -774,11 +774,11 @@ | |||||||
|  |  | ||||||
|     "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], |     "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], | ||||||
|  |  | ||||||
|     "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="], |     "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], | ||||||
|  |  | ||||||
|     "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], |     "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], | ||||||
|  |  | ||||||
|     "prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="], |     "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], | ||||||
|  |  | ||||||
|     "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], |     "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], | ||||||
|  |  | ||||||
| @@ -792,9 +792,9 @@ | |||||||
|  |  | ||||||
|     "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], |     "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], | ||||||
|  |  | ||||||
|     "react-hook-form": ["react-hook-form@7.57.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-RbEks3+cbvTP84l/VXGUZ+JMrKOS8ykQCRYdm5aYsxnDquL0vspsyNhGRO7pcH6hsZqWlPOjLye7rJqdtdAmlg=="], |     "react-hook-form": ["react-hook-form@7.60.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A=="], | ||||||
|  |  | ||||||
|     "react-i18next": ["react-i18next@15.5.2", "", { "dependencies": { "@babel/runtime": "^7.25.0", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-ePODyXgmZQAOYTbZXQn5rRsSBu3Gszo69jxW6aKmlSgxKAI1fOhDwSu6bT4EKHciWPKQ7v7lPrjeiadR6Gi+1A=="], |     "react-i18next": ["react-i18next@15.6.0", "", { "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-W135dB0rDfiFmbMipC17nOhGdttO5mzH8BivY+2ybsQBbXvxWIwl3cmeH3T9d+YPBSJu/ouyJKFJTtkK7rJofw=="], | ||||||
|  |  | ||||||
|     "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=="], |     "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=="], | ||||||
|  |  | ||||||
| @@ -804,7 +804,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-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.6.2", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w=="], |     "react-router": ["react-router@7.7.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-3FUYSwlvB/5wRJVTL/aavqHmfUKe0+Xm9MllkYgGo9eDwNdkvwlJGjpPxono1kCycLt6AnDTgjmXvK3/B4QGuw=="], | ||||||
|  |  | ||||||
|     "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=="], |     "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=="], | ||||||
|  |  | ||||||
| @@ -830,7 +830,7 @@ | |||||||
|  |  | ||||||
|     "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], |     "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], | ||||||
|  |  | ||||||
|     "sonner": ["sonner@2.0.5", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-YwbHQO6cSso3HBXlbCkgrgzDNIhws14r4MO87Ofy+cV2X7ES4pOoAK3+veSmVTvqNx1BWUxlhPmZzP00Crk2aQ=="], |     "sonner": ["sonner@2.0.6", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-yHFhk8T/DK3YxjFQXIrcHT1rGEeTLliVzWbO0xN8GberVun2RiBnxAjXAYpZrqwEVHBG9asI/Li8TAAhN9m59Q=="], | ||||||
|  |  | ||||||
|     "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], |     "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], | ||||||
|  |  | ||||||
| @@ -846,15 +846,15 @@ | |||||||
|  |  | ||||||
|     "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], |     "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], | ||||||
|  |  | ||||||
|     "tailwind-merge": ["tailwind-merge@3.3.0", "", {}, "sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ=="], |     "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="], | ||||||
|  |  | ||||||
|     "tailwindcss": ["tailwindcss@4.1.8", "", {}, "sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og=="], |     "tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="], | ||||||
|  |  | ||||||
|     "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], |     "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.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.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="], |     "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], | ||||||
|  |  | ||||||
|     "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], |     "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], | ||||||
|  |  | ||||||
| @@ -866,15 +866,15 @@ | |||||||
|  |  | ||||||
|     "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], |     "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], | ||||||
|  |  | ||||||
|     "tw-animate-css": ["tw-animate-css@1.3.4", "", {}, "sha512-dd1Ht6/YQHcNbq0znIT6dG8uhO7Ce+VIIhZUhjsryXsMPJQz3bZg7Q2eNzLwipb25bRZslGb2myio5mScd1TFg=="], |     "tw-animate-css": ["tw-animate-css@1.3.5", "", {}, "sha512-t3u+0YNoloIhj1mMXs779P6MO9q3p3mvGn4k1n3nJPqJw/glZcuijG2qTSN4z4mgNRfW5ZC3aXJFLwDtiipZXA=="], | ||||||
|  |  | ||||||
|     "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], |     "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], | ||||||
|  |  | ||||||
|     "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], |     "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], | ||||||
|  |  | ||||||
|     "typescript-eslint": ["typescript-eslint@8.34.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.34.0", "@typescript-eslint/parser": "8.34.0", "@typescript-eslint/utils": "8.34.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-MRpfN7uYjTrTGigFCt8sRyNqJFhjN0WwZecldaqhWm+wy0gaRt8Edb/3cuUy0zdq2opJWT6iXINKAtewnDOltQ=="], |     "typescript-eslint": ["typescript-eslint@8.37.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.37.0", "@typescript-eslint/parser": "8.37.0", "@typescript-eslint/typescript-estree": "8.37.0", "@typescript-eslint/utils": "8.37.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA=="], | ||||||
|  |  | ||||||
|     "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], |     "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], | ||||||
|  |  | ||||||
|     "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=="], |     "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=="], | ||||||
|  |  | ||||||
| @@ -900,7 +900,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=="], |     "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@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "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-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="], |     "vite": ["vite@7.0.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.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-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw=="], | ||||||
|  |  | ||||||
|     "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], |     "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], | ||||||
|  |  | ||||||
| @@ -912,7 +912,7 @@ | |||||||
|  |  | ||||||
|     "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], |     "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], | ||||||
|  |  | ||||||
|     "zod": ["zod@3.25.57", "", {}, "sha512-6tgzLuwVST5oLUxXTmBqoinKMd3JeesgbgseXeFasKKj8Q1FCZrHnbqJOyiEvr4cVAlbug+CgIsmJ8cl/pU5FA=="], |     "zod": ["zod@4.0.5", "", {}, "sha512-/5UuuRPStvHXu7RS+gmvRf4NXrNxpSllGwDnCBcJZtQsKrviYXm54yDGV2KYNLT5kq0lHGcl7lqWJLgSaG+tgA=="], | ||||||
|  |  | ||||||
|     "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], |     "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], | ||||||
|  |  | ||||||
| @@ -928,11 +928,13 @@ | |||||||
|  |  | ||||||
|     "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], |     "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], | ||||||
|  |  | ||||||
|  |     "@eslint/eslintrc/espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="], | ||||||
|  |  | ||||||
|     "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], |     "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], | ||||||
|  |  | ||||||
|     "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], |     "@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.14.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg=="], | ||||||
|  |  | ||||||
|     "@tailwindcss/node/lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], |     "@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=="], |     "@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=="], | ||||||
|  |  | ||||||
| @@ -940,7 +942,7 @@ | |||||||
|  |  | ||||||
|     "@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/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="], | ||||||
|  |  | ||||||
|     "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.10", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" }, "bundled": true }, "sha512-bCsCyeZEwVErsGmyPNSzwfwFn4OdxBj0mmv6hOFucB/k81Ojdu68RbZdxYsRQUPc9l6SU5F/cG+bXgWs3oUgsQ=="], |     "@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/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], |     "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], | ||||||
|  |  | ||||||
| @@ -958,43 +960,47 @@ | |||||||
|  |  | ||||||
|     "@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=="], |     "@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.34.0", "", { "dependencies": { "@typescript-eslint/types": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0" } }, "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw=="], |     "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.37.0", "", { "dependencies": { "@typescript-eslint/types": "8.37.0", "@typescript-eslint/visitor-keys": "8.37.0" } }, "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.34.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.34.0", "@typescript-eslint/types": "8.34.0", "@typescript-eslint/typescript-estree": "8.34.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ=="], |     "@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.37.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.37.0", "@typescript-eslint/types": "8.37.0", "@typescript-eslint/typescript-estree": "8.37.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.4", "", {}, "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="], |     "@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.34.0", "", { "dependencies": { "@typescript-eslint/types": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0" } }, "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw=="], |     "@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.37.0", "", { "dependencies": { "@typescript-eslint/types": "8.37.0", "@typescript-eslint/visitor-keys": "8.37.0" } }, "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.34.0", "", {}, "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA=="], |     "@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.37.0", "", {}, "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/parser/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.34.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.34.0", "@typescript-eslint/tsconfig-utils": "8.34.0", "@typescript-eslint/types": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.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-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg=="], |     "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.37.0", "", {}, "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.34.0", "", {}, "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA=="], |     "@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.34.1", "", { "dependencies": { "@typescript-eslint/types": "8.34.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.32.0", "", { "dependencies": { "@typescript-eslint/types": "8.32.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w=="], |     "@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.37.0", "", {}, "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.34.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.34.0", "@typescript-eslint/tsconfig-utils": "8.34.0", "@typescript-eslint/types": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.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-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg=="], |     "@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.37.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.37.0", "@typescript-eslint/types": "8.37.0", "@typescript-eslint/typescript-estree": "8.37.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.34.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.34.0", "@typescript-eslint/types": "8.34.0", "@typescript-eslint/typescript-estree": "8.34.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ=="], |     "@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.37.0", "", {}, "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.32.0", "", { "dependencies": { "@typescript-eslint/types": "8.32.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w=="], |  | ||||||
|  |  | ||||||
|     "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], |     "@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/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.34.0", "", {}, "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA=="], |     "@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.34.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.34.1", "@typescript-eslint/tsconfig-utils": "8.34.1", "@typescript-eslint/types": "8.34.1", "@typescript-eslint/visitor-keys": "8.34.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 <5.9.0" } }, "sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA=="], | ||||||
|  |  | ||||||
|  |     "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.37.0", "", {}, "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ=="], | ||||||
|  |  | ||||||
|     "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], |     "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], | ||||||
|  |  | ||||||
|  |     "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=="], | ||||||
|  |  | ||||||
|     "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], |     "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], | ||||||
|  |  | ||||||
|     "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], |     "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], | ||||||
|  |  | ||||||
|     "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], |     "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], | ||||||
|  |  | ||||||
|     "typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.34.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.34.0", "@typescript-eslint/types": "8.34.0", "@typescript-eslint/typescript-estree": "8.34.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ=="], |     "typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.37.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.37.0", "@typescript-eslint/types": "8.37.0", "@typescript-eslint/typescript-estree": "8.37.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A=="], | ||||||
|  |  | ||||||
|     "@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/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=="], | ||||||
|  |  | ||||||
| @@ -1002,25 +1008,9 @@ | |||||||
|  |  | ||||||
|     "@babel/helper-module-imports/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], |     "@babel/helper-module-imports/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], | ||||||
|  |  | ||||||
|     "@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], |     "@eslint/eslintrc/espree/acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], | ||||||
|  |  | ||||||
|     "@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="], |     "@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], | ||||||
|  |  | ||||||
|     "@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="], |  | ||||||
|  |  | ||||||
|     "@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="], |  | ||||||
|  |  | ||||||
|     "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="], |  | ||||||
|  |  | ||||||
|     "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="], |  | ||||||
|  |  | ||||||
|     "@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="], |  | ||||||
|  |  | ||||||
|     "@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="], |  | ||||||
|  |  | ||||||
|     "@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="], |  | ||||||
|  |  | ||||||
|     "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="], |  | ||||||
|  |  | ||||||
|     "@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/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="], | ||||||
|  |  | ||||||
| @@ -1032,50 +1022,30 @@ | |||||||
|  |  | ||||||
|     "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], |     "@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.34.0", "", {}, "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA=="], |     "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.37.0", "", {}, "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.34.0", "", {}, "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA=="], |     "@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.37.0", "", {}, "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.34.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.34.0", "@typescript-eslint/tsconfig-utils": "8.34.0", "@typescript-eslint/types": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.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-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg=="], |     "@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.37.0", "", { "dependencies": { "@typescript-eslint/types": "8.37.0", "@typescript-eslint/visitor-keys": "8.37.0" } }, "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/parser/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], |  | ||||||
|  |  | ||||||
|     "@typescript-eslint/parser/@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], |  | ||||||
|  |  | ||||||
|     "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.34.0", "", {}, "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA=="], |  | ||||||
|  |  | ||||||
|     "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], |  | ||||||
|  |  | ||||||
|     "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], |  | ||||||
|  |  | ||||||
|     "@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.34.0", "", { "dependencies": { "@typescript-eslint/types": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0" } }, "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw=="], |  | ||||||
|  |  | ||||||
|     "@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.34.0", "", {}, "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA=="], |  | ||||||
|  |  | ||||||
|     "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], |     "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], | ||||||
|  |  | ||||||
|     "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.34.0", "", { "dependencies": { "@typescript-eslint/types": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0" } }, "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw=="], |     "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.34.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.34.1", "@typescript-eslint/types": "^8.34.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA=="], | ||||||
|  |  | ||||||
|     "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.34.0", "", {}, "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA=="], |     "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.34.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg=="], | ||||||
|  |  | ||||||
|     "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.34.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.34.0", "@typescript-eslint/tsconfig-utils": "8.34.0", "@typescript-eslint/types": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.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-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg=="], |     "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.34.1", "", { "dependencies": { "@typescript-eslint/types": "8.34.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw=="], | ||||||
|  |  | ||||||
|  |     "@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.37.0", "", { "dependencies": { "@typescript-eslint/types": "8.37.0", "@typescript-eslint/visitor-keys": "8.37.0" } }, "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA=="], | ||||||
|  |  | ||||||
|  |     "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.37.0", "", {}, "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ=="], | ||||||
|  |  | ||||||
|     "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], |     "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@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/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], |     "@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=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], |  | ||||||
|  |  | ||||||
|     "@typescript-eslint/parser/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], |  | ||||||
|  |  | ||||||
|     "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], |  | ||||||
|  |  | ||||||
|     "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], |  | ||||||
|  |  | ||||||
|     "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], |  | ||||||
|  |  | ||||||
|     "@typescript-eslint/eslint-plugin/@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=="], |  | ||||||
|  |  | ||||||
|     "typescript-eslint/@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=="], |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -15,44 +15,44 @@ | |||||||
|     "@radix-ui/react-select": "^2.2.5", |     "@radix-ui/react-select": "^2.2.5", | ||||||
|     "@radix-ui/react-separator": "^1.1.7", |     "@radix-ui/react-separator": "^1.1.7", | ||||||
|     "@radix-ui/react-slot": "^1.2.3", |     "@radix-ui/react-slot": "^1.2.3", | ||||||
|     "@tailwindcss/vite": "^4.1.8", |     "@tailwindcss/vite": "^4.1.11", | ||||||
|     "@tanstack/react-query": "^5.80.6", |     "@tanstack/react-query": "^5.83.0", | ||||||
|     "axios": "^1.9.0", |     "axios": "^1.10.0", | ||||||
|     "class-variance-authority": "^0.7.1", |     "class-variance-authority": "^0.7.1", | ||||||
|     "clsx": "^2.1.1", |     "clsx": "^2.1.1", | ||||||
|     "dompurify": "^3.2.6", |     "dompurify": "^3.2.6", | ||||||
|     "i18next": "^25.2.1", |     "i18next": "^25.3.2", | ||||||
|     "i18next-browser-languagedetector": "^8.0.5", |     "i18next-browser-languagedetector": "^8.2.0", | ||||||
|     "i18next-resources-to-backend": "^1.2.1", |     "i18next-resources-to-backend": "^1.2.1", | ||||||
|     "input-otp": "^1.4.2", |     "input-otp": "^1.4.2", | ||||||
|     "lucide-react": "^0.513.0", |     "lucide-react": "^0.525.0", | ||||||
|     "next-themes": "^0.4.6", |     "next-themes": "^0.4.6", | ||||||
|     "react": "^19.0.0", |     "react": "^19.0.0", | ||||||
|     "react-dom": "^19.0.0", |     "react-dom": "^19.0.0", | ||||||
|     "react-hook-form": "^7.57.0", |     "react-hook-form": "^7.60.0", | ||||||
|     "react-i18next": "^15.5.2", |     "react-i18next": "^15.6.0", | ||||||
|     "react-markdown": "^10.1.0", |     "react-markdown": "^10.1.0", | ||||||
|     "react-router": "^7.6.2", |     "react-router": "^7.7.0", | ||||||
|     "sonner": "^2.0.5", |     "sonner": "^2.0.6", | ||||||
|     "tailwind-merge": "^3.3.0", |     "tailwind-merge": "^3.3.1", | ||||||
|     "tailwindcss": "^4.1.8", |     "tailwindcss": "^4.1.11", | ||||||
|     "zod": "^3.25.57" |     "zod": "^4.0.5" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@eslint/js": "^9.28.0", |     "@eslint/js": "^9.31.0", | ||||||
|     "@tanstack/eslint-plugin-query": "^5.78.0", |     "@tanstack/eslint-plugin-query": "^5.81.2", | ||||||
|     "@types/node": "^22.15.29", |     "@types/node": "^24.0.14", | ||||||
|     "@types/react": "^19.1.7", |     "@types/react": "^19.1.8", | ||||||
|     "@types/react-dom": "^19.1.6", |     "@types/react-dom": "^19.1.6", | ||||||
|     "@vitejs/plugin-react": "^4.5.2", |     "@vitejs/plugin-react": "^4.6.0", | ||||||
|     "eslint": "^9.28.0", |     "eslint": "^9.31.0", | ||||||
|     "eslint-plugin-react-hooks": "^5.2.0", |     "eslint-plugin-react-hooks": "^5.2.0", | ||||||
|     "eslint-plugin-react-refresh": "^0.4.19", |     "eslint-plugin-react-refresh": "^0.4.19", | ||||||
|     "globals": "^16.2.0", |     "globals": "^16.3.0", | ||||||
|     "prettier": "3.5.3", |     "prettier": "3.6.2", | ||||||
|     "tw-animate-css": "^1.3.4", |     "tw-animate-css": "^1.3.5", | ||||||
|     "typescript": "~5.8.3", |     "typescript": "~5.8.3", | ||||||
|     "typescript-eslint": "^8.34.0", |     "typescript-eslint": "^8.37.0", | ||||||
|     "vite": "^6.3.1" |     "vite": "^7.0.5" | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -12,6 +12,7 @@ import { | |||||||
| } from "../ui/form"; | } from "../ui/form"; | ||||||
| import { Button } from "../ui/button"; | import { Button } from "../ui/button"; | ||||||
| import { loginSchema, LoginSchema } from "@/schemas/login-schema"; | import { loginSchema, LoginSchema } from "@/schemas/login-schema"; | ||||||
|  | import z from "zod"; | ||||||
|  |  | ||||||
| interface Props { | interface Props { | ||||||
|   onSubmit: (data: LoginSchema) => void; |   onSubmit: (data: LoginSchema) => void; | ||||||
| @@ -22,6 +23,11 @@ export const LoginForm = (props: Props) => { | |||||||
|   const { onSubmit, loading } = props; |   const { onSubmit, loading } = props; | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|  |  | ||||||
|  |   z.config({ | ||||||
|  |     customError: (iss) => | ||||||
|  |       iss.input === undefined ? t("fieldRequired") : t("invalidInput"), | ||||||
|  |   }); | ||||||
|  |  | ||||||
|   const form = useForm<LoginSchema>({ |   const form = useForm<LoginSchema>({ | ||||||
|     resolver: zodResolver(loginSchema), |     resolver: zodResolver(loginSchema), | ||||||
|   }); |   }); | ||||||
| @@ -33,12 +39,13 @@ export const LoginForm = (props: Props) => { | |||||||
|           control={form.control} |           control={form.control} | ||||||
|           name="username" |           name="username" | ||||||
|           render={({ field }) => ( |           render={({ field }) => ( | ||||||
|             <FormItem className="mb-4"> |             <FormItem className="mb-4 gap-0"> | ||||||
|               <FormLabel>{t("loginUsername")}</FormLabel> |               <FormLabel className="mb-2">{t("loginUsername")}</FormLabel> | ||||||
|               <FormControl> |               <FormControl className="mb-1"> | ||||||
|                 <Input |                 <Input | ||||||
|                   placeholder={t("loginUsername")} |                   placeholder={t("loginUsername")} | ||||||
|                   disabled={loading} |                   disabled={loading} | ||||||
|  |                   autoComplete="username" | ||||||
|                   {...field} |                   {...field} | ||||||
|                 /> |                 /> | ||||||
|               </FormControl> |               </FormControl> | ||||||
| @@ -50,25 +57,26 @@ export const LoginForm = (props: Props) => { | |||||||
|           control={form.control} |           control={form.control} | ||||||
|           name="password" |           name="password" | ||||||
|           render={({ field }) => ( |           render={({ field }) => ( | ||||||
|             <FormItem className="mb-4"> |             <FormItem className="mb-4 gap-0"> | ||||||
|               <div className="relative"> |               <div className="relative mb-1"> | ||||||
|                 <FormLabel className="mb-2">{t("loginPassword")}</FormLabel> |                 <FormLabel className="mb-2">{t("loginPassword")}</FormLabel> | ||||||
|                 <FormControl> |                 <FormControl> | ||||||
|                   <Input |                   <Input | ||||||
|                     placeholder={t("loginPassword")} |                     placeholder={t("loginPassword")} | ||||||
|                     type="password" |                     type="password" | ||||||
|                     disabled={loading} |                     disabled={loading} | ||||||
|  |                     autoComplete="current-password" | ||||||
|                     {...field} |                     {...field} | ||||||
|                   /> |                   /> | ||||||
|                 </FormControl> |                 </FormControl> | ||||||
|                 <FormMessage /> |  | ||||||
|                 <a |                 <a | ||||||
|                   href="/forgot-password" |                   href="/forgot-password" | ||||||
|                   className="text-muted-foreground text-sm absolute right-0 bottom-10" |                   className="text-muted-foreground text-sm absolute right-0 bottom-[2.565rem]" // 2.565 is *just* perfect | ||||||
|                 > |                 > | ||||||
|                   {t("forgotPasswordTitle")} |                   {t("forgotPasswordTitle")} | ||||||
|                 </a> |                 </a> | ||||||
|               </div> |               </div> | ||||||
|  |               <FormMessage /> | ||||||
|             </FormItem> |             </FormItem> | ||||||
|           )} |           )} | ||||||
|         /> |         /> | ||||||
|   | |||||||
| @@ -8,6 +8,8 @@ import { | |||||||
| import { zodResolver } from "@hookform/resolvers/zod"; | import { zodResolver } from "@hookform/resolvers/zod"; | ||||||
| import { useForm } from "react-hook-form"; | import { useForm } from "react-hook-form"; | ||||||
| import { totpSchema, TotpSchema } from "@/schemas/totp-schema"; | import { totpSchema, TotpSchema } from "@/schemas/totp-schema"; | ||||||
|  | import { useTranslation } from "react-i18next"; | ||||||
|  | import z from "zod"; | ||||||
|  |  | ||||||
| interface Props { | interface Props { | ||||||
|   formId: string; |   formId: string; | ||||||
| @@ -17,6 +19,12 @@ interface Props { | |||||||
|  |  | ||||||
| export const TotpForm = (props: Props) => { | export const TotpForm = (props: Props) => { | ||||||
|   const { formId, onSubmit, loading } = props; |   const { formId, onSubmit, loading } = props; | ||||||
|  |   const { t } = useTranslation(); | ||||||
|  |  | ||||||
|  |   z.config({ | ||||||
|  |     customError: (iss) => | ||||||
|  |       iss.input === undefined ? t("fieldRequired") : t("invalidInput"), | ||||||
|  |   }); | ||||||
|  |  | ||||||
|   const form = useForm<TotpSchema>({ |   const form = useForm<TotpSchema>({ | ||||||
|     resolver: zodResolver(totpSchema), |     resolver: zodResolver(totpSchema), | ||||||
| @@ -31,7 +39,12 @@ export const TotpForm = (props: Props) => { | |||||||
|           render={({ field }) => ( |           render={({ field }) => ( | ||||||
|             <FormItem> |             <FormItem> | ||||||
|               <FormControl> |               <FormControl> | ||||||
|                 <InputOTP maxLength={6} disabled={loading} {...field}> |                 <InputOTP | ||||||
|  |                   maxLength={6} | ||||||
|  |                   disabled={loading} | ||||||
|  |                   {...field} | ||||||
|  |                   autoComplete="one-time-code" | ||||||
|  |                 > | ||||||
|                   <InputOTPGroup> |                   <InputOTPGroup> | ||||||
|                     <InputOTPSlot index={0} /> |                     <InputOTPSlot index={0} /> | ||||||
|                     <InputOTPSlot index={1} /> |                     <InputOTPSlot index={1} /> | ||||||
|   | |||||||
| @@ -136,7 +136,7 @@ h4 { | |||||||
| } | } | ||||||
|  |  | ||||||
| p { | p { | ||||||
|   @apply leading-6 [&:not(:first-child)]:mt-6; |   @apply leading-6; | ||||||
| } | } | ||||||
|  |  | ||||||
| blockquote { | blockquote { | ||||||
|   | |||||||
| @@ -27,8 +27,8 @@ export const languages = { | |||||||
|   "tr-TR": "Türkçe", |   "tr-TR": "Türkçe", | ||||||
|   "uk-UA": "Українська", |   "uk-UA": "Українська", | ||||||
|   "vi-VN": "Tiếng Việt", |   "vi-VN": "Tiếng Việt", | ||||||
|   "zh-CN": "中文", |   "zh-CN": "简体中文", | ||||||
|   "zh-TW": "中文", |   "zh-TW": "繁體中文(台灣)", | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export type SupportedLanguage = keyof typeof languages; | export type SupportedLanguage = keyof typeof languages; | ||||||
|   | |||||||
| @@ -14,14 +14,17 @@ | |||||||
|     "loginOauthFailSubtitle": "Failed to get OAuth URL", |     "loginOauthFailSubtitle": "Failed to get OAuth URL", | ||||||
|     "loginOauthSuccessTitle": "Redirecting", |     "loginOauthSuccessTitle": "Redirecting", | ||||||
|     "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", |     "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...", |     "continueRedirectingTitle": "Redirecting...", | ||||||
|     "continueRedirectingSubtitle": "You should be redirected to the app soon", |     "continueRedirectingSubtitle": "You should be redirected to the app soon", | ||||||
|     "continueInvalidRedirectTitle": "Invalid redirect", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", |  | ||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueInsecureRedirectTitle": "Insecure redirect", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", | ||||||
|     "continueTitle": "Continue", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueSubtitle": "Click the button to continue to your app.", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "Failed to log out", |     "logoutFailTitle": "Failed to log out", | ||||||
|     "logoutFailSubtitle": "Please try again", |     "logoutFailSubtitle": "Please try again", | ||||||
|     "logoutSuccessTitle": "Logged out", |     "logoutSuccessTitle": "Logged out", | ||||||
| @@ -42,12 +45,18 @@ | |||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||||
|  |     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |  | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |  | ||||||
|     "cancelTitle": "Cancel", |     "cancelTitle": "Cancel", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "forgotPasswordTitle": "Forgot your password?", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||||
|     "errorTitle": "An error occurred", |     "errorTitle": "An error occurred", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "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", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
|     "loginTitle": "مرحبا بعودتك، قم بتسجيل الدخول باستخدام", |     "loginTitle": "مرحبا بعودتك، ادخل باستخدام", | ||||||
|     "loginTitleSimple": "Welcome back, please login", |     "loginTitleSimple": "مرحبا بعودتك، سجل دخولك", | ||||||
|     "loginDivider": "Or", |     "loginDivider": "أو", | ||||||
|     "loginUsername": "اسم المستخدم", |     "loginUsername": "اسم المستخدم", | ||||||
|     "loginPassword": "كلمة المرور", |     "loginPassword": "كلمة المرور", | ||||||
|     "loginSubmit": "تسجيل الدخول", |     "loginSubmit": "تسجيل الدخول", | ||||||
| @@ -10,18 +10,21 @@ | |||||||
|     "loginFailRateLimit": "You failed to login too many times. Please try again later", |     "loginFailRateLimit": "You failed to login too many times. Please try again later", | ||||||
|     "loginSuccessTitle": "تم تسجيل الدخول", |     "loginSuccessTitle": "تم تسجيل الدخول", | ||||||
|     "loginSuccessSubtitle": "مرحبا بعودتك!", |     "loginSuccessSubtitle": "مرحبا بعودتك!", | ||||||
|     "loginOauthFailTitle": "An error occurred", |     "loginOauthFailTitle": "حدث خطأ", | ||||||
|     "loginOauthFailSubtitle": "فشل في الحصول على رابط OAuth", |     "loginOauthFailSubtitle": "أخفق الحصول على رابط OAuth", | ||||||
|     "loginOauthSuccessTitle": "إعادة توجيه", |     "loginOauthSuccessTitle": "إعادة توجيه", | ||||||
|     "loginOauthSuccessSubtitle": "إعادة توجيه إلى مزود OAuth الخاص بك", |     "loginOauthSuccessSubtitle": "إعادة توجيه إلى مزود OAuth الخاص بك", | ||||||
|  |     "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", | ||||||
|  |     "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", | ||||||
|  |     "loginOauthAutoRedirectButton": "Redirect now", | ||||||
|  |     "continueTitle": "متابعة", | ||||||
|     "continueRedirectingTitle": "إعادة توجيه...", |     "continueRedirectingTitle": "إعادة توجيه...", | ||||||
|     "continueRedirectingSubtitle": "يجب إعادة توجيهك إلى التطبيق قريبا", |     "continueRedirectingSubtitle": "يجب إعادة توجيهك إلى التطبيق قريبا", | ||||||
|     "continueInvalidRedirectTitle": "إعادة توجيه غير صالحة", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueInvalidRedirectSubtitle": "رابط إعادة التوجيه غير صالح", |  | ||||||
|     "continueInsecureRedirectTitle": "إعادة توجيه غير آمنة", |     "continueInsecureRedirectTitle": "إعادة توجيه غير آمنة", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "أنت تحاول إعادة التوجيه من <code>https</code> إلى <code>http</code>، هل أنت متأكد أنك تريد المتابعة؟", | ||||||
|     "continueTitle": "متابعة", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueSubtitle": "انقر الزر للمتابعة إلى التطبيق الخاص بك.", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "فشل تسجيل الخروج", |     "logoutFailTitle": "فشل تسجيل الخروج", | ||||||
|     "logoutFailSubtitle": "يرجى إعادة المحاولة", |     "logoutFailSubtitle": "يرجى إعادة المحاولة", | ||||||
|     "logoutSuccessTitle": "تم تسجيل الخروج", |     "logoutSuccessTitle": "تم تسجيل الخروج", | ||||||
| @@ -32,7 +35,7 @@ | |||||||
|     "notFoundTitle": "الصفحة غير موجودة", |     "notFoundTitle": "الصفحة غير موجودة", | ||||||
|     "notFoundSubtitle": "الصفحة التي تبحث عنها غير موجودة.", |     "notFoundSubtitle": "الصفحة التي تبحث عنها غير موجودة.", | ||||||
|     "notFoundButton": "انتقل إلى الرئيسية", |     "notFoundButton": "انتقل إلى الرئيسية", | ||||||
|     "totpFailTitle": "فشل في التحقق من الرمز", |     "totpFailTitle": "أخفق التحقق من الرمز", | ||||||
|     "totpFailSubtitle": "الرجاء التحقق من الرمز الخاص بك وحاول مرة أخرى", |     "totpFailSubtitle": "الرجاء التحقق من الرمز الخاص بك وحاول مرة أخرى", | ||||||
|     "totpSuccessTitle": "تم التحقق", |     "totpSuccessTitle": "تم التحقق", | ||||||
|     "totpSuccessSubtitle": "إعادة توجيه إلى تطبيقك", |     "totpSuccessSubtitle": "إعادة توجيه إلى تطبيقك", | ||||||
| @@ -42,12 +45,18 @@ | |||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||||
|  |     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "حاول مجددا", |     "unauthorizedButton": "حاول مجددا", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |  | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |  | ||||||
|     "cancelTitle": "إلغاء", |     "cancelTitle": "إلغاء", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "forgotPasswordTitle": "نسيت كلمة المرور؟", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||||
|     "errorTitle": "An error occurred", |     "errorTitle": "حدث خطأ", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "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", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "تجاهل", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -14,14 +14,17 @@ | |||||||
|     "loginOauthFailSubtitle": "Failed to get OAuth URL", |     "loginOauthFailSubtitle": "Failed to get OAuth URL", | ||||||
|     "loginOauthSuccessTitle": "Redirecting", |     "loginOauthSuccessTitle": "Redirecting", | ||||||
|     "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", |     "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...", |     "continueRedirectingTitle": "Redirecting...", | ||||||
|     "continueRedirectingSubtitle": "You should be redirected to the app soon", |     "continueRedirectingSubtitle": "You should be redirected to the app soon", | ||||||
|     "continueInvalidRedirectTitle": "Invalid redirect", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", |  | ||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueInsecureRedirectTitle": "Insecure redirect", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", | ||||||
|     "continueTitle": "Continue", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueSubtitle": "Click the button to continue to your app.", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "Failed to log out", |     "logoutFailTitle": "Failed to log out", | ||||||
|     "logoutFailSubtitle": "Please try again", |     "logoutFailSubtitle": "Please try again", | ||||||
|     "logoutSuccessTitle": "Logged out", |     "logoutSuccessTitle": "Logged out", | ||||||
| @@ -42,12 +45,18 @@ | |||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||||
|  |     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |  | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |  | ||||||
|     "cancelTitle": "Cancel", |     "cancelTitle": "Cancel", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "forgotPasswordTitle": "Forgot your password?", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||||
|     "errorTitle": "An error occurred", |     "errorTitle": "An error occurred", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "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", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -1,53 +1,62 @@ | |||||||
| { | { | ||||||
|     "loginTitle": "Welcome back, login with", |     "loginTitle": "Vítejte zpět, přihlaste se pomocí", | ||||||
|     "loginTitleSimple": "Welcome back, please login", |     "loginTitleSimple": "Vítejte zpět, přihlaste se prosím", | ||||||
|     "loginDivider": "Or", |     "loginDivider": "Nebo", | ||||||
|     "loginUsername": "Username", |     "loginUsername": "Uživatelské jméno", | ||||||
|     "loginPassword": "Password", |     "loginPassword": "Heslo", | ||||||
|     "loginSubmit": "Login", |     "loginSubmit": "Přihlásit", | ||||||
|     "loginFailTitle": "Failed to log in", |     "loginFailTitle": "Přihlášení se nezdařilo", | ||||||
|     "loginFailSubtitle": "Please check your username and password", |     "loginFailSubtitle": "Zkontrolujte prosím své uživatelské jméno a heslo", | ||||||
|     "loginFailRateLimit": "You failed to login too many times. Please try again later", |     "loginFailRateLimit": "Přiliš mnoho neúspěšných pokusů přihlášení. Zkuste to prosím později", | ||||||
|     "loginSuccessTitle": "Logged in", |     "loginSuccessTitle": "Přihlášen", | ||||||
|     "loginSuccessSubtitle": "Welcome back!", |     "loginSuccessSubtitle": "Vítejte zpět!", | ||||||
|     "loginOauthFailTitle": "An error occurred", |     "loginOauthFailTitle": "Došlo k chybě", | ||||||
|     "loginOauthFailSubtitle": "Failed to get OAuth URL", |     "loginOauthFailSubtitle": "Nepodařilo se získat OAuth URL", | ||||||
|     "loginOauthSuccessTitle": "Redirecting", |     "loginOauthSuccessTitle": "Přesměrování", | ||||||
|     "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", |     "loginOauthSuccessSubtitle": "Přesměrování k poskytovateli OAuth", | ||||||
|     "continueRedirectingTitle": "Redirecting...", |     "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", | ||||||
|     "continueRedirectingSubtitle": "You should be redirected to the app soon", |     "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", | ||||||
|     "continueInvalidRedirectTitle": "Invalid redirect", |     "loginOauthAutoRedirectButton": "Redirect now", | ||||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", |     "continueTitle": "Pokračovat", | ||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueRedirectingTitle": "Přesměrování...", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueRedirectingSubtitle": "Brzy budete přesměrováni do aplikace", | ||||||
|     "continueTitle": "Continue", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueSubtitle": "Click the button to continue to your app.", |     "continueInsecureRedirectTitle": "Nezabezpečené přesměrování", | ||||||
|     "logoutFailTitle": "Failed to log out", |     "continueInsecureRedirectSubtitle": "Pokoušíte se přesměrovat z <code>https</code> na <code>http</code>, které není bezpečné. Opravdu chcete pokračovat?", | ||||||
|     "logoutFailSubtitle": "Please try again", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "logoutSuccessTitle": "Logged out", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutSuccessSubtitle": "You have been logged out", |     "logoutFailTitle": "Odhlášení se nezdařilo", | ||||||
|     "logoutTitle": "Logout", |     "logoutFailSubtitle": "Zkuste to prosím znovu", | ||||||
|     "logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.", |     "logoutSuccessTitle": "Odhlášen", | ||||||
|     "logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.", |     "logoutSuccessSubtitle": "Byl jste odhlášen", | ||||||
|     "notFoundTitle": "Page not found", |     "logoutTitle": "Odhlásit", | ||||||
|     "notFoundSubtitle": "The page you are looking for does not exist.", |     "logoutUsernameSubtitle": "Jste přihlášen jako <code>{{username}}</code>. Pro odhlášení klikněte na tlačítko níže.", | ||||||
|     "notFoundButton": "Go home", |     "logoutOauthSubtitle": "Jste přihlášen jako <code>{{username}}</code> pomocí {{provider}} poskytovatele OAuth. Pro odhlášení klikněte na tlačítko níže.", | ||||||
|     "totpFailTitle": "Failed to verify code", |     "notFoundTitle": "Stránka nenalezena", | ||||||
|     "totpFailSubtitle": "Please check your code and try again", |     "notFoundSubtitle": "Stránka, kterou hledáte, neexistuje.", | ||||||
|     "totpSuccessTitle": "Verified", |     "notFoundButton": "Jít domů", | ||||||
|     "totpSuccessSubtitle": "Redirecting to your app", |     "totpFailTitle": "Nepodařilo se ověřit kód", | ||||||
|     "totpTitle": "Enter your TOTP code", |     "totpFailSubtitle": "Zkontrolujte prosím kód a zkuste to znovu", | ||||||
|     "totpSubtitle": "Please enter the code from your authenticator app.", |     "totpSuccessTitle": "Ověřeno", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "totpSuccessSubtitle": "Přesměrování do aplikace", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "totpTitle": "Zadejte TOTP kód", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "totpSubtitle": "Zadejte prosím kód z ověřovací aplikace.", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedTitle": "Nepovoleno", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedResourceSubtitle": "Uživatel s uživatelským jménem <code>{{username}}</code> není oprávněn k přístupu ke zdroji <code>{{resource}}</code>.", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "unauthorizedLoginSubtitle": "Uživatel s uživatelským jménem <code>{{username}}</code> není oprávněn k přihlášení.", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |     "unauthorizedGroupsSubtitle": "Uživatel s uživatelským jménem <code>{{username}}</code> není ve skupině potřebné k přístupu ke zdroji <code>{{resource}}</code>.", | ||||||
|     "cancelTitle": "Cancel", |     "unauthorizedIpSubtitle": "Vaše IP adresa <code>{{ip}}</code> není oprávněna k přístupu ke zdroji <code>{{resource}}</code>.", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "unauthorizedButton": "Zkusit znovu", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "cancelTitle": "Zrušit", | ||||||
|     "errorTitle": "An error occurred", |     "forgotPasswordTitle": "Zapomněli jste heslo?", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "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 <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -14,14 +14,17 @@ | |||||||
|     "loginOauthFailSubtitle": "Kunne ikke hente OAuth-URL", |     "loginOauthFailSubtitle": "Kunne ikke hente OAuth-URL", | ||||||
|     "loginOauthSuccessTitle": "Omdirigerer", |     "loginOauthSuccessTitle": "Omdirigerer", | ||||||
|     "loginOauthSuccessSubtitle": "Omdirigerer til din OAuth-udbyder", |     "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...", |     "continueRedirectingTitle": "Omdirigerer...", | ||||||
|     "continueRedirectingSubtitle": "Du bør blive omdirigeret til appen snart", |     "continueRedirectingSubtitle": "Du bør blive omdirigeret til appen snart", | ||||||
|     "continueInvalidRedirectTitle": "Ugyldig omdirigering", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueInvalidRedirectSubtitle": "Omdirigerings-URL'en er ugyldig", |  | ||||||
|     "continueInsecureRedirectTitle": "Usikker omdirigering", |     "continueInsecureRedirectTitle": "Usikker omdirigering", | ||||||
|     "continueInsecureRedirectSubtitle": "Du forsøger at omdirigere fra <code>https</code> til <code>http</code>, som ikke er sikker. Er du sikker på, at du vil fortsætte?", |     "continueInsecureRedirectSubtitle": "Du forsøger at omdirigere fra <code>https</code> til <code>http</code>, som ikke er sikker. Er du sikker på, at du vil fortsætte?", | ||||||
|     "continueTitle": "Fortsæt", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueSubtitle": "Klik på knappen for at fortsætte til din app.", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "Log ud mislykkedes", |     "logoutFailTitle": "Log ud mislykkedes", | ||||||
|     "logoutFailSubtitle": "Prøv venligst igen", |     "logoutFailSubtitle": "Prøv venligst igen", | ||||||
|     "logoutSuccessTitle": "Logget ud", |     "logoutSuccessTitle": "Logget ud", | ||||||
| @@ -42,12 +45,18 @@ | |||||||
|     "unauthorizedResourceSubtitle": "Brugeren med brugernavnet <code>{{username}}</code> har ikke tilladelse til at tilgå ressourcen <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "Brugeren med brugernavnet <code>{{username}}</code> har ikke tilladelse til at tilgå ressourcen <code>{{resource}}</code>.", | ||||||
|     "unauthorizedLoginSubtitle": "Brugeren med brugernavnet <code>{{username}}</code> har ikke tilladelse til at logge ind.", |     "unauthorizedLoginSubtitle": "Brugeren med brugernavnet <code>{{username}}</code> har ikke tilladelse til at logge ind.", | ||||||
|     "unauthorizedGroupsSubtitle": "Brugeren med brugernavnet <code>{{username}}</code> er ikke i de grupper, som ressourcen <code>{{resource}}</code> kræver.", |     "unauthorizedGroupsSubtitle": "Brugeren med brugernavnet <code>{{username}}</code> er ikke i de grupper, som ressourcen <code>{{resource}}</code> kræver.", | ||||||
|  |     "unauthorizedIpSubtitle": "Din IP adresse <code>{{ip}}</code> er ikke autoriseret til at tilgå ressourcen <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Prøv igen", |     "unauthorizedButton": "Prøv igen", | ||||||
|     "untrustedRedirectTitle": "Usikker omdirigering", |  | ||||||
|     "untrustedRedirectSubtitle": "Du forsøger at omdirigere til et domæne, der ikke matcher dit konfigurerede domæne (<code>{{domain}}</code>). Er du sikker på, at du vil fortsætte?", |  | ||||||
|     "cancelTitle": "Annuller", |     "cancelTitle": "Annuller", | ||||||
|     "forgotPasswordTitle": "Glemt din adgangskode?", |     "forgotPasswordTitle": "Glemt din adgangskode?", | ||||||
|     "failedToFetchProvidersTitle": "Kunne ikke indlæse godkendelsesudbydere. Tjek venligst din konfiguration.", |     "failedToFetchProvidersTitle": "Kunne ikke indlæse godkendelsesudbydere. Tjek venligst din konfiguration.", | ||||||
|     "errorTitle": "Der opstod en fejl", |     "errorTitle": "Der opstod en fejl", | ||||||
|     "errorSubtitle": "Der opstod en fejl under forsøget på at udføre denne handling. Tjek venligst konsollen for mere information." |     "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", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -7,47 +7,56 @@ | |||||||
|     "loginSubmit": "Anmelden", |     "loginSubmit": "Anmelden", | ||||||
|     "loginFailTitle": "Login fehlgeschlagen", |     "loginFailTitle": "Login fehlgeschlagen", | ||||||
|     "loginFailSubtitle": "Bitte überprüfe deinen Benutzernamen und Passwort", |     "loginFailSubtitle": "Bitte überprüfe deinen Benutzernamen und Passwort", | ||||||
|     "loginFailRateLimit": "You failed to login too many times. Please try again later", |     "loginFailRateLimit": "Zu viele fehlgeschlagene Loginversuche. Versuche es später erneut", | ||||||
|     "loginSuccessTitle": "Angemeldet", |     "loginSuccessTitle": "Angemeldet", | ||||||
|     "loginSuccessSubtitle": "Willkommen zurück!", |     "loginSuccessSubtitle": "Willkommen zurück!", | ||||||
|     "loginOauthFailTitle": "Ein Fehler ist aufgetreten", |     "loginOauthFailTitle": "Ein Fehler ist aufgetreten", | ||||||
|     "loginOauthFailSubtitle": "Fehler beim Abrufen der OAuth-URL", |     "loginOauthFailSubtitle": "Fehler beim Abrufen der OAuth-URL", | ||||||
|     "loginOauthSuccessTitle": "Leite weiter", |     "loginOauthSuccessTitle": "Leite weiter", | ||||||
|     "loginOauthSuccessSubtitle": "Weiterleitung zu Ihrem OAuth-Provider", |     "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...", |     "continueRedirectingTitle": "Leite weiter...", | ||||||
|     "continueRedirectingSubtitle": "Sie sollten in Kürze zur App weitergeleitet werden", |     "continueRedirectingSubtitle": "Sie sollten in Kürze zur App weitergeleitet werden", | ||||||
|     "continueInvalidRedirectTitle": "Ungültige Weiterleitung", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueInvalidRedirectSubtitle": "Die Weiterleitungs-URL ist ungültig", |  | ||||||
|     "continueInsecureRedirectTitle": "Unsichere Weiterleitung", |     "continueInsecureRedirectTitle": "Unsichere Weiterleitung", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "Sie versuchen von <code>https</code> auf <code>http</code> weiterzuleiten, was unsicher ist. Sind Sie sicher, dass Sie fortfahren möchten?", | ||||||
|     "continueTitle": "Weiter", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueSubtitle": "Klicken Sie auf den Button, um zur App zu gelangen.", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "Abmelden fehlgeschlagen", |     "logoutFailTitle": "Abmelden fehlgeschlagen", | ||||||
|     "logoutFailSubtitle": "Bitte versuchen Sie es erneut", |     "logoutFailSubtitle": "Bitte versuchen Sie es erneut", | ||||||
|     "logoutSuccessTitle": "Abgemeldet", |     "logoutSuccessTitle": "Abgemeldet", | ||||||
|     "logoutSuccessSubtitle": "Sie wurden abgemeldet", |     "logoutSuccessSubtitle": "Sie wurden abgemeldet", | ||||||
|     "logoutTitle": "Abmelden", |     "logoutTitle": "Abmelden", | ||||||
|     "logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.", |     "logoutUsernameSubtitle": "Sie sind derzeit als <code>{{username}}</code> angemeldet. Klicken Sie auf den Button unten, um sich abzumelden.", | ||||||
|     "logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.", |     "logoutOauthSubtitle": "Sie sind derzeit als <code>{{username}}</code> über den OAuth-Anbieter {{provider}} angemeldet. Klicken Sie auf den Button unten, um sich abzumelden.", | ||||||
|     "notFoundTitle": "Seite nicht gefunden", |     "notFoundTitle": "Seite nicht gefunden", | ||||||
|     "notFoundSubtitle": "Die gesuchte Seite existiert nicht.", |     "notFoundSubtitle": "Die gesuchte Seite existiert nicht.", | ||||||
|     "notFoundButton": "Nach Hause", |     "notFoundButton": "Zurück", | ||||||
|     "totpFailTitle": "Fehler beim Verifizieren des Codes", |     "totpFailTitle": "Fehler beim Verifizieren des Codes", | ||||||
|     "totpFailSubtitle": "Bitte überprüfen Sie Ihren Code und versuchen Sie es erneut", |     "totpFailSubtitle": "Bitte überprüfen Sie Ihren Code und versuchen Sie es erneut", | ||||||
|     "totpSuccessTitle": "Verifiziert", |     "totpSuccessTitle": "Verifiziert", | ||||||
|     "totpSuccessSubtitle": "Leite zur App weiter", |     "totpSuccessSubtitle": "Leite zur App weiter", | ||||||
|     "totpTitle": "Geben Sie Ihren TOTP Code ein", |     "totpTitle": "Geben Sie Ihren TOTP Code ein", | ||||||
|     "totpSubtitle": "Please enter the code from your authenticator app.", |     "totpSubtitle": "Bitte geben Sie den Code aus Ihrer Authenticator-App ein.", | ||||||
|     "unauthorizedTitle": "Unautorisiert", |     "unauthorizedTitle": "Unautorisiert", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "Der Benutzer mit Benutzername <code>{{username}}</code> ist nicht berechtigt, auf die Ressource <code>{{resource}}</code> zuzugreifen.", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "Der Benutzer mit Benutzername <code>{{username}}</code> ist nicht berechtigt, sich anzumelden.", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "Der Benutzer mit Benutzername <code>{{username}}</code> ist nicht in den Gruppen, die von der Ressource <code>{{resource}}</code> benötigt werden.", | ||||||
|  |     "unauthorizedIpSubtitle": "Ihre IP-Adresse <code>{{ip}}</code> ist nicht berechtigt, auf die Ressource <code>{{resource}}</code> zuzugreifen.", | ||||||
|     "unauthorizedButton": "Erneut versuchen", |     "unauthorizedButton": "Erneut versuchen", | ||||||
|     "untrustedRedirectTitle": "Nicht vertrauenswürdige Weiterleitung", |  | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |  | ||||||
|     "cancelTitle": "Abbrechen", |     "cancelTitle": "Abbrechen", | ||||||
|     "forgotPasswordTitle": "Passwort vergessen?", |     "forgotPasswordTitle": "Passwort vergessen?", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "failedToFetchProvidersTitle": "Fehler beim Laden der Authentifizierungsanbieter. Bitte überprüfen Sie Ihre Konfiguration.", | ||||||
|     "errorTitle": "An error occurred", |     "errorTitle": "Ein Fehler ist aufgetreten", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "errorSubtitle": "Beim Versuch, diese Aktion auszuführen, ist ein Fehler aufgetreten. Bitte überprüfen Sie die Konsole für weitere Informationen.", | ||||||
|  |     "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 <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -14,14 +14,17 @@ | |||||||
|     "loginOauthFailSubtitle": "Αποτυχία λήψης OAuth URL", |     "loginOauthFailSubtitle": "Αποτυχία λήψης OAuth URL", | ||||||
|     "loginOauthSuccessTitle": "Ανακατεύθυνση", |     "loginOauthSuccessTitle": "Ανακατεύθυνση", | ||||||
|     "loginOauthSuccessSubtitle": "Ανακατεύθυνση στον πάροχο OAuth σας", |     "loginOauthSuccessSubtitle": "Ανακατεύθυνση στον πάροχο OAuth σας", | ||||||
|  |     "loginOauthAutoRedirectTitle": "Αυτόματη Ανακατεύθυνση OAuth", | ||||||
|  |     "loginOauthAutoRedirectSubtitle": "Θα ανακατευθυνθείτε αυτόματα στον πάροχο OAuth σας για να επαληθευτείτε.", | ||||||
|  |     "loginOauthAutoRedirectButton": "Ανακατεύθυνση τώρα", | ||||||
|  |     "continueTitle": "Συνέχεια", | ||||||
|     "continueRedirectingTitle": "Ανακατεύθυνση...", |     "continueRedirectingTitle": "Ανακατεύθυνση...", | ||||||
|     "continueRedirectingSubtitle": "Θα πρέπει να μεταφερθείτε σύντομα στην εφαρμογή σας", |     "continueRedirectingSubtitle": "Θα πρέπει να μεταφερθείτε σύντομα στην εφαρμογή σας", | ||||||
|     "continueInvalidRedirectTitle": "Μη έγκυρη ανακατεύθυνση", |     "continueRedirectManually": "Χειροκίνητη ανακατεύθυνση", | ||||||
|     "continueInvalidRedirectSubtitle": "Το URL ανακατεύθυνσης δεν είναι έγκυρο", |  | ||||||
|     "continueInsecureRedirectTitle": "Μη ασφαλής ανακατεύθυνση", |     "continueInsecureRedirectTitle": "Μη ασφαλής ανακατεύθυνση", | ||||||
|     "continueInsecureRedirectSubtitle": "Προσπαθείτε να ανακατευθύνετε από <code>https</code> σε <code>http</code> το οποίο δεν είναι ασφαλές. Είστε σίγουροι ότι θέλετε να συνεχίσετε;", |     "continueInsecureRedirectSubtitle": "Προσπαθείτε να ανακατευθύνετε από <code>https</code> σε <code>http</code> το οποίο δεν είναι ασφαλές. Είστε σίγουροι ότι θέλετε να συνεχίσετε;", | ||||||
|     "continueTitle": "Συνέχεια", |     "continueUntrustedRedirectTitle": "Μη έμπιστη ανακατεύθυνση", | ||||||
|     "continueSubtitle": "Κάντε κλικ στο κουμπί για να συνεχίσετε στην εφαρμογή σας.", |     "continueUntrustedRedirectSubtitle": "Προσπαθείτε να ανακατευθύνετε σε ένα domain που δεν ταιριάζει με το ρυθμισμένο domain σας (<code>{{cookieDomain}}</code>). Είστε βέβαιοι ότι θέλετε να συνεχίσετε;", | ||||||
|     "logoutFailTitle": "Αποτυχία αποσύνδεσης", |     "logoutFailTitle": "Αποτυχία αποσύνδεσης", | ||||||
|     "logoutFailSubtitle": "Παρακαλώ δοκιμάστε ξανά", |     "logoutFailSubtitle": "Παρακαλώ δοκιμάστε ξανά", | ||||||
|     "logoutSuccessTitle": "Αποσυνδεδεμένος", |     "logoutSuccessTitle": "Αποσυνδεδεμένος", | ||||||
| @@ -42,12 +45,18 @@ | |||||||
|     "unauthorizedResourceSubtitle": "Ο χρήστης με όνομα χρήστη <code>{{username}}</code> δεν έχει άδεια πρόσβασης στον πόρο <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "Ο χρήστης με όνομα χρήστη <code>{{username}}</code> δεν έχει άδεια πρόσβασης στον πόρο <code>{{resource}}</code>.", | ||||||
|     "unauthorizedLoginSubtitle": "Ο χρήστης με όνομα χρήστη <code>{{username}}</code> δεν είναι εξουσιοδοτημένος να συνδεθεί.", |     "unauthorizedLoginSubtitle": "Ο χρήστης με όνομα χρήστη <code>{{username}}</code> δεν είναι εξουσιοδοτημένος να συνδεθεί.", | ||||||
|     "unauthorizedGroupsSubtitle": "Ο χρήστης με όνομα χρήστη <code>{{username}}</code> δεν είναι στις ομάδες που απαιτούνται από τον πόρο <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "Ο χρήστης με όνομα χρήστη <code>{{username}}</code> δεν είναι στις ομάδες που απαιτούνται από τον πόρο <code>{{resource}}</code>.", | ||||||
|  |     "unauthorizedIpSubtitle": "Η διεύθυνση IP σας <code>{{ip}}</code> δεν είναι εξουσιοδοτημένη να έχει πρόσβαση στον πόρο <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Προσπαθήστε ξανά", |     "unauthorizedButton": "Προσπαθήστε ξανά", | ||||||
|     "untrustedRedirectTitle": "Μη έμπιστη ανακατεύθυνση", |  | ||||||
|     "untrustedRedirectSubtitle": "Προσπαθείτε να ανακατευθύνετε σε ένα domain που δεν ταιριάζει με τον ρυθμισμένο domain σας (<code>{{domain}}</code>). Είστε βέβαιοι ότι θέλετε να συνεχίσετε;", |  | ||||||
|     "cancelTitle": "Ακύρωση", |     "cancelTitle": "Ακύρωση", | ||||||
|     "forgotPasswordTitle": "Ξεχάσατε το συνθηματικό σας;", |     "forgotPasswordTitle": "Ξεχάσατε το συνθηματικό σας;", | ||||||
|     "failedToFetchProvidersTitle": "Αποτυχία φόρτωσης παρόχων πιστοποίησης. Παρακαλώ ελέγξτε τις ρυθμίσεις σας.", |     "failedToFetchProvidersTitle": "Αποτυχία φόρτωσης παρόχων πιστοποίησης. Παρακαλώ ελέγξτε τις ρυθμίσεις σας.", | ||||||
|     "errorTitle": "Παρουσιάστηκε ένα σφάλμα", |     "errorTitle": "Παρουσιάστηκε ένα σφάλμα", | ||||||
|     "errorSubtitle": "Παρουσιάστηκε σφάλμα κατά την προσπάθεια εκτέλεσης αυτής της ενέργειας. Ελέγξτε την κονσόλα για περισσότερες πληροφορίες." |     "errorSubtitle": "Παρουσιάστηκε σφάλμα κατά την προσπάθεια εκτέλεσης αυτής της ενέργειας. Ελέγξτε την κονσόλα για περισσότερες πληροφορίες.", | ||||||
|  |     "forgotPasswordMessage": "Μπορείτε να επαναφέρετε τον κωδικό πρόσβασής σας αλλάζοντας τη μεταβλητή περιβάλλοντος `USERS`.", | ||||||
|  |     "fieldRequired": "Αυτό το πεδίο είναι υποχρεωτικό", | ||||||
|  |     "invalidInput": "Μη έγκυρη καταχώρηση", | ||||||
|  |     "domainWarningTitle": "Μη έγκυρο domain", | ||||||
|  |     "domainWarningSubtitle": "Αυτή η εφαρμογή έχει ρυθμιστεί για πρόσβαση από <code>{{appUrl}}</code>, αλλά <code>{{currentUrl}}</code> χρησιμοποιείται. Αν συνεχίσετε, μπορεί να αντιμετωπίσετε προβλήματα με την ταυτοποίηση.", | ||||||
|  |     "ignoreTitle": "Παράβλεψη", | ||||||
|  |     "goToCorrectDomainTitle": "Μεταβείτε στο σωστό domain" | ||||||
| } | } | ||||||
| @@ -14,14 +14,17 @@ | |||||||
|     "loginOauthFailSubtitle": "Failed to get OAuth URL", |     "loginOauthFailSubtitle": "Failed to get OAuth URL", | ||||||
|     "loginOauthSuccessTitle": "Redirecting", |     "loginOauthSuccessTitle": "Redirecting", | ||||||
|     "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", |     "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...", |     "continueRedirectingTitle": "Redirecting...", | ||||||
|     "continueRedirectingSubtitle": "You should be redirected to the app soon", |     "continueRedirectingSubtitle": "You should be redirected to the app soon", | ||||||
|     "continueInvalidRedirectTitle": "Invalid redirect", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", |  | ||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueInsecureRedirectTitle": "Insecure redirect", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", | ||||||
|     "continueTitle": "Continue", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueSubtitle": "Click the button to continue to your app.", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "Failed to log out", |     "logoutFailTitle": "Failed to log out", | ||||||
|     "logoutFailSubtitle": "Please try again", |     "logoutFailSubtitle": "Please try again", | ||||||
|     "logoutSuccessTitle": "Logged out", |     "logoutSuccessTitle": "Logged out", | ||||||
| @@ -42,12 +45,18 @@ | |||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||||
|  |     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |  | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |  | ||||||
|     "cancelTitle": "Cancel", |     "cancelTitle": "Cancel", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "forgotPasswordTitle": "Forgot your password?", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||||
|     "errorTitle": "An error occurred", |     "errorTitle": "An error occurred", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "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", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -42,6 +42,7 @@ | |||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||||
|  |     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", | ||||||
| @@ -49,5 +50,8 @@ | |||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "forgotPasswordTitle": "Forgot your password?", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||||
|     "errorTitle": "An error occurred", |     "errorTitle": "An error occurred", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "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" | ||||||
| } | } | ||||||
| @@ -1,53 +1,62 @@ | |||||||
| { | { | ||||||
|     "loginTitle": "Bienvenido de nuevo, inicie sesión con", |     "loginTitle": "Bienvenido de vuelta, inicie sesión con", | ||||||
|     "loginTitleSimple": "Welcome back, please login", |     "loginTitleSimple": "Bienvenido de vuelta, por favor inicie sesión", | ||||||
|     "loginDivider": "Or", |     "loginDivider": "O", | ||||||
|     "loginUsername": "Username", |     "loginUsername": "Usuario", | ||||||
|     "loginPassword": "Password", |     "loginPassword": "Contraseña", | ||||||
|     "loginSubmit": "Login", |     "loginSubmit": "Iniciar sesión", | ||||||
|     "loginFailTitle": "Failed to log in", |     "loginFailTitle": "Fallo al iniciar sesión", | ||||||
|     "loginFailSubtitle": "Please check your username and password", |     "loginFailSubtitle": "Por favor revise su usuario y contraseña", | ||||||
|     "loginFailRateLimit": "You failed to login too many times. Please try again later", |     "loginFailRateLimit": "Muchos inicios de sesión consecutivos fallidos. Por favor inténtelo más tarde", | ||||||
|     "loginSuccessTitle": "Logged in", |     "loginSuccessTitle": "Sesión iniciada", | ||||||
|     "loginSuccessSubtitle": "Welcome back!", |     "loginSuccessSubtitle": "¡Bienvenido de vuelta!", | ||||||
|     "loginOauthFailTitle": "An error occurred", |     "loginOauthFailTitle": "Ocurrió un error", | ||||||
|     "loginOauthFailSubtitle": "Error al obtener la URL de OAuth", |     "loginOauthFailSubtitle": "Error al obtener la URL de OAuth", | ||||||
|     "loginOauthSuccessTitle": "Redirecting", |     "loginOauthSuccessTitle": "Redireccionando", | ||||||
|     "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", |     "loginOauthSuccessSubtitle": "Redireccionando a tu proveedor de OAuth", | ||||||
|     "continueRedirectingTitle": "Redirecting...", |     "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", | ||||||
|     "continueRedirectingSubtitle": "You should be redirected to the app soon", |     "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", | ||||||
|     "continueInvalidRedirectTitle": "Invalid redirect", |     "loginOauthAutoRedirectButton": "Redirect now", | ||||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", |     "continueTitle": "Continuar", | ||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueRedirectingTitle": "Redireccionando...", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueRedirectingSubtitle": "Pronto será redirigido a la aplicación", | ||||||
|     "continueTitle": "Continue", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueSubtitle": "Click the button to continue to your app.", |     "continueInsecureRedirectTitle": "Redirección insegura", | ||||||
|     "logoutFailTitle": "Failed to log out", |     "continueInsecureRedirectSubtitle": "Está intentando redirigir desde <code>https</code> a <code>http</code> lo cual no es seguro. ¿Está seguro que desea continuar?", | ||||||
|     "logoutFailSubtitle": "Please try again", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "logoutSuccessTitle": "Logged out", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutSuccessSubtitle": "You have been logged out", |     "logoutFailTitle": "Fallo al cerrar sesión", | ||||||
|     "logoutTitle": "Logout", |     "logoutFailSubtitle": "Por favor intente nuevamente", | ||||||
|     "logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.", |     "logoutSuccessTitle": "Sesión cerrada", | ||||||
|     "logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.", |     "logoutSuccessSubtitle": "Su sesión ha sido cerrada", | ||||||
|     "notFoundTitle": "Page not found", |     "logoutTitle": "Cerrar sesión", | ||||||
|     "notFoundSubtitle": "The page you are looking for does not exist.", |     "logoutUsernameSubtitle": "Actualmente está conectado como <code>{{username}}</code>. Haga clic en el botón de abajo para cerrar sesión.", | ||||||
|     "notFoundButton": "Go home", |     "logoutOauthSubtitle": "Actualmente está conectado como <code>{{username}}</code> usando {{provider}} como su proveedor de OAuth. Haga clic en el botón de abajo para cerrar sesión.", | ||||||
|     "totpFailTitle": "Failed to verify code", |     "notFoundTitle": "Página no encontrada", | ||||||
|     "totpFailSubtitle": "Please check your code and try again", |     "notFoundSubtitle": "La página que está buscando no existe.", | ||||||
|     "totpSuccessTitle": "Verified", |     "notFoundButton": "Volver al inicio", | ||||||
|     "totpSuccessSubtitle": "Redirecting to your app", |     "totpFailTitle": "Error al verificar código", | ||||||
|     "totpTitle": "Enter your TOTP code", |     "totpFailSubtitle": "Por favor compruebe su código e inténtelo de nuevo", | ||||||
|     "totpSubtitle": "Please enter the code from your authenticator app.", |     "totpSuccessTitle": "Verificado", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "totpSuccessSubtitle": "Redirigiendo a su aplicación", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "totpTitle": "Ingrese su código TOTP", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "totpSubtitle": "Por favor introduzca el código de su aplicación de autenticación.", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedTitle": "No autorizado", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedResourceSubtitle": "El usuario con nombre de usuario <code>{{username}}</code> no está autorizado para acceder al recurso <code>{{resource}}</code>.", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "unauthorizedLoginSubtitle": "El usuario con nombre de usuario <code>{{username}}</code> no está autorizado a iniciar sesión.", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |     "unauthorizedGroupsSubtitle": "El usuario con nombre de usuario <code>{{username}}</code> no está en los grupos requeridos por el recurso <code>{{resource}}</code>.", | ||||||
|     "cancelTitle": "Cancel", |     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "unauthorizedButton": "Inténtelo de nuevo", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "cancelTitle": "Cancelar", | ||||||
|     "errorTitle": "An error occurred", |     "forgotPasswordTitle": "¿Olvidó su contraseña?", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "failedToFetchProvidersTitle": "Error al cargar los proveedores de autenticación. Por favor revise su configuración.", | ||||||
|  |     "errorTitle": "Ha ocurrido un error", | ||||||
|  |     "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", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -14,14 +14,17 @@ | |||||||
|     "loginOauthFailSubtitle": "Failed to get OAuth URL", |     "loginOauthFailSubtitle": "Failed to get OAuth URL", | ||||||
|     "loginOauthSuccessTitle": "Redirecting", |     "loginOauthSuccessTitle": "Redirecting", | ||||||
|     "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", |     "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...", |     "continueRedirectingTitle": "Redirecting...", | ||||||
|     "continueRedirectingSubtitle": "You should be redirected to the app soon", |     "continueRedirectingSubtitle": "You should be redirected to the app soon", | ||||||
|     "continueInvalidRedirectTitle": "Invalid redirect", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", |  | ||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueInsecureRedirectTitle": "Insecure redirect", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", | ||||||
|     "continueTitle": "Continue", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueSubtitle": "Click the button to continue to your app.", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "Failed to log out", |     "logoutFailTitle": "Failed to log out", | ||||||
|     "logoutFailSubtitle": "Please try again", |     "logoutFailSubtitle": "Please try again", | ||||||
|     "logoutSuccessTitle": "Logged out", |     "logoutSuccessTitle": "Logged out", | ||||||
| @@ -42,12 +45,18 @@ | |||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||||
|  |     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |  | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |  | ||||||
|     "cancelTitle": "Cancel", |     "cancelTitle": "Cancel", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "forgotPasswordTitle": "Forgot your password?", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||||
|     "errorTitle": "An error occurred", |     "errorTitle": "An error occurred", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "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", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -1,34 +1,37 @@ | |||||||
| { | { | ||||||
|     "loginTitle": "Bienvenue, connectez-vous avec", |     "loginTitle": "Bienvenue, connectez-vous avec", | ||||||
|     "loginTitleSimple": "Welcome back, please login", |     "loginTitleSimple": "De retour parmi nous, veuillez vous connecter", | ||||||
|     "loginDivider": "Or", |     "loginDivider": "Ou", | ||||||
|     "loginUsername": "Nom d'utilisateur", |     "loginUsername": "Nom d'utilisateur", | ||||||
|     "loginPassword": "Mot de passe", |     "loginPassword": "Mot de passe", | ||||||
|     "loginSubmit": "Se connecter", |     "loginSubmit": "Se connecter", | ||||||
|     "loginFailTitle": "Échec de la connexion", |     "loginFailTitle": "Échec de la connexion", | ||||||
|     "loginFailSubtitle": "Veuillez vérifier votre nom d'utilisateur et votre mot de passe", |     "loginFailSubtitle": "Veuillez vérifier votre nom d'utilisateur et votre mot de passe", | ||||||
|     "loginFailRateLimit": "You failed to login too many times. Please try again later", |     "loginFailRateLimit": "Vous avez échoué trop de fois à vous connecter. Veuillez réessayer ultérieurement", | ||||||
|     "loginSuccessTitle": "Connecté", |     "loginSuccessTitle": "Connecté", | ||||||
|     "loginSuccessSubtitle": "Bienvenue!", |     "loginSuccessSubtitle": "Bienvenue !", | ||||||
|     "loginOauthFailTitle": "An error occurred", |     "loginOauthFailTitle": "Une erreur s'est produite", | ||||||
|     "loginOauthFailSubtitle": "Impossible d'obtenir l'URL OAuth", |     "loginOauthFailSubtitle": "Impossible d'obtenir l'URL OAuth", | ||||||
|     "loginOauthSuccessTitle": "Redirection", |     "loginOauthSuccessTitle": "Redirection", | ||||||
|     "loginOauthSuccessSubtitle": "Redirection vers votre fournisseur OAuth", |     "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...", |     "continueRedirectingTitle": "Redirection...", | ||||||
|     "continueRedirectingSubtitle": "Vous devriez être redirigé vers l'application bientôt", |     "continueRedirectingSubtitle": "Vous devriez être redirigé vers l'application bientôt", | ||||||
|     "continueInvalidRedirectTitle": "Redirection invalide", |     "continueRedirectManually": "Redirection manuelle", | ||||||
|     "continueInvalidRedirectSubtitle": "L'URL de redirection est invalide", |  | ||||||
|     "continueInsecureRedirectTitle": "Redirection non sécurisée", |     "continueInsecureRedirectTitle": "Redirection non sécurisée", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "Vous tentez de rediriger de <code>https</code> vers <code>http</code>, ce qui n'est pas sécurisé. Êtes-vous sûr de vouloir continuer ?", | ||||||
|     "continueTitle": "Continuer", |     "continueUntrustedRedirectTitle": "Redirection non sécurisée", | ||||||
|     "continueSubtitle": "Cliquez sur le bouton pour continuer vers votre application.", |     "continueUntrustedRedirectSubtitle": "Vous essayez de rediriger vers un domaine qui ne correspond pas à votre domaine configuré (<code>{{cookieDomain}}</code>). Êtes-vous sûr de vouloir continuer ?", | ||||||
|     "logoutFailTitle": "Échec de la déconnexion", |     "logoutFailTitle": "Échec de la déconnexion", | ||||||
|     "logoutFailSubtitle": "Veuillez réessayer", |     "logoutFailSubtitle": "Veuillez réessayer", | ||||||
|     "logoutSuccessTitle": "Déconnecté", |     "logoutSuccessTitle": "Déconnecté", | ||||||
|     "logoutSuccessSubtitle": "Vous avez été déconnecté", |     "logoutSuccessSubtitle": "Vous avez été déconnecté", | ||||||
|     "logoutTitle": "Déconnexion", |     "logoutTitle": "Déconnexion", | ||||||
|     "logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.", |     "logoutUsernameSubtitle": "Vous êtes actuellement connecté en tant que <code>{{username}}</code>. Cliquez sur le bouton ci-dessous pour vous déconnecter.", | ||||||
|     "logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.", |     "logoutOauthSubtitle": "Vous êtes actuellement connecté en tant que <code>{{username}}</code> via le fournisseur OAuth {{provider}}. Cliquez sur le bouton ci-dessous pour vous déconnecter.", | ||||||
|     "notFoundTitle": "Page introuvable", |     "notFoundTitle": "Page introuvable", | ||||||
|     "notFoundSubtitle": "La page recherchée n'existe pas.", |     "notFoundSubtitle": "La page recherchée n'existe pas.", | ||||||
|     "notFoundButton": "Retour à la page d'accueil", |     "notFoundButton": "Retour à la page d'accueil", | ||||||
| @@ -37,17 +40,23 @@ | |||||||
|     "totpSuccessTitle": "Vérifié", |     "totpSuccessTitle": "Vérifié", | ||||||
|     "totpSuccessSubtitle": "Redirection vers votre application", |     "totpSuccessSubtitle": "Redirection vers votre application", | ||||||
|     "totpTitle": "Saisissez votre code TOTP", |     "totpTitle": "Saisissez votre code TOTP", | ||||||
|     "totpSubtitle": "Please enter the code from your authenticator app.", |     "totpSubtitle": "Veuillez saisir le code de votre application d'authentification.", | ||||||
|     "unauthorizedTitle": "Non autorisé", |     "unauthorizedTitle": "Non autorisé", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "L'utilisateur avec le nom d'utilisateur <code>{{username}}</code> n'est pas autorisé à accéder à la ressource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "L'utilisateur avec le nom d'utilisateur <code>{{username}}</code> n'est pas autorisé à se connecter.", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "L'utilisateur avec le nom d'utilisateur <code>{{username}}</code> n'appartient pas aux groupes requis par la ressource <code>{{resource}}</code>.", | ||||||
|  |     "unauthorizedIpSubtitle": "Votre adresse IP <code>{{ip}}</code> n'est pas autorisée à accéder à la ressource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Réessayer", |     "unauthorizedButton": "Réessayer", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "cancelTitle": "Annuler", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |     "forgotPasswordTitle": "Mot de passe oublié ?", | ||||||
|     "cancelTitle": "Cancel", |     "failedToFetchProvidersTitle": "Échec du chargement des fournisseurs d'authentification. Veuillez vérifier votre configuration.", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "errorTitle": "Une erreur est survenue", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "errorSubtitle": "Une erreur est survenue lors de l'exécution de cette action. Veuillez consulter la console pour plus d'informations.", | ||||||
|     "errorTitle": "An error occurred", |     "forgotPasswordMessage": "Vous pouvez réinitialiser votre mot de passe en modifiant la variable d'environnement `USERS`.", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "fieldRequired": "Ce champ est obligatoire", | ||||||
|  |     "invalidInput": "Saisie non valide", | ||||||
|  |     "domainWarningTitle": "Domaine invalide", | ||||||
|  |     "domainWarningSubtitle": "Cette instance est configurée pour être accédée depuis <code>{{appUrl}}</code>, mais <code>{{currentUrl}}</code> est utilisé. Si vous continuez, vous pourriez rencontrer des problèmes d'authentification.", | ||||||
|  |     "ignoreTitle": "Ignorer", | ||||||
|  |     "goToCorrectDomainTitle": "Aller au bon domaine" | ||||||
| } | } | ||||||
| @@ -14,14 +14,17 @@ | |||||||
|     "loginOauthFailSubtitle": "Failed to get OAuth URL", |     "loginOauthFailSubtitle": "Failed to get OAuth URL", | ||||||
|     "loginOauthSuccessTitle": "Redirecting", |     "loginOauthSuccessTitle": "Redirecting", | ||||||
|     "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", |     "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...", |     "continueRedirectingTitle": "Redirecting...", | ||||||
|     "continueRedirectingSubtitle": "You should be redirected to the app soon", |     "continueRedirectingSubtitle": "You should be redirected to the app soon", | ||||||
|     "continueInvalidRedirectTitle": "Invalid redirect", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", |  | ||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueInsecureRedirectTitle": "Insecure redirect", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", | ||||||
|     "continueTitle": "Continue", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueSubtitle": "Click the button to continue to your app.", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "Failed to log out", |     "logoutFailTitle": "Failed to log out", | ||||||
|     "logoutFailSubtitle": "Please try again", |     "logoutFailSubtitle": "Please try again", | ||||||
|     "logoutSuccessTitle": "Logged out", |     "logoutSuccessTitle": "Logged out", | ||||||
| @@ -42,12 +45,18 @@ | |||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||||
|  |     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |  | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |  | ||||||
|     "cancelTitle": "Cancel", |     "cancelTitle": "Cancel", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "forgotPasswordTitle": "Forgot your password?", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||||
|     "errorTitle": "An error occurred", |     "errorTitle": "An error occurred", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "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", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -14,14 +14,17 @@ | |||||||
|     "loginOauthFailSubtitle": "Failed to get OAuth URL", |     "loginOauthFailSubtitle": "Failed to get OAuth URL", | ||||||
|     "loginOauthSuccessTitle": "Redirecting", |     "loginOauthSuccessTitle": "Redirecting", | ||||||
|     "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", |     "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...", |     "continueRedirectingTitle": "Redirecting...", | ||||||
|     "continueRedirectingSubtitle": "You should be redirected to the app soon", |     "continueRedirectingSubtitle": "You should be redirected to the app soon", | ||||||
|     "continueInvalidRedirectTitle": "Invalid redirect", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", |  | ||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueInsecureRedirectTitle": "Insecure redirect", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", | ||||||
|     "continueTitle": "Continue", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueSubtitle": "Click the button to continue to your app.", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "Failed to log out", |     "logoutFailTitle": "Failed to log out", | ||||||
|     "logoutFailSubtitle": "Please try again", |     "logoutFailSubtitle": "Please try again", | ||||||
|     "logoutSuccessTitle": "Logged out", |     "logoutSuccessTitle": "Logged out", | ||||||
| @@ -42,12 +45,18 @@ | |||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||||
|  |     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |  | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |  | ||||||
|     "cancelTitle": "Cancel", |     "cancelTitle": "Cancel", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "forgotPasswordTitle": "Forgot your password?", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||||
|     "errorTitle": "An error occurred", |     "errorTitle": "An error occurred", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "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", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -14,14 +14,17 @@ | |||||||
|     "loginOauthFailSubtitle": "Failed to get OAuth URL", |     "loginOauthFailSubtitle": "Failed to get OAuth URL", | ||||||
|     "loginOauthSuccessTitle": "Redirecting", |     "loginOauthSuccessTitle": "Redirecting", | ||||||
|     "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", |     "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...", |     "continueRedirectingTitle": "Redirecting...", | ||||||
|     "continueRedirectingSubtitle": "You should be redirected to the app soon", |     "continueRedirectingSubtitle": "You should be redirected to the app soon", | ||||||
|     "continueInvalidRedirectTitle": "Invalid redirect", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", |  | ||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueInsecureRedirectTitle": "Insecure redirect", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", | ||||||
|     "continueTitle": "Continue", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueSubtitle": "Click the button to continue to your app.", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "Failed to log out", |     "logoutFailTitle": "Failed to log out", | ||||||
|     "logoutFailSubtitle": "Please try again", |     "logoutFailSubtitle": "Please try again", | ||||||
|     "logoutSuccessTitle": "Logged out", |     "logoutSuccessTitle": "Logged out", | ||||||
| @@ -42,12 +45,18 @@ | |||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||||
|  |     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |  | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |  | ||||||
|     "cancelTitle": "Cancel", |     "cancelTitle": "Cancel", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "forgotPasswordTitle": "Forgot your password?", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||||
|     "errorTitle": "An error occurred", |     "errorTitle": "An error occurred", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "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", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -14,14 +14,17 @@ | |||||||
|     "loginOauthFailSubtitle": "Failed to get OAuth URL", |     "loginOauthFailSubtitle": "Failed to get OAuth URL", | ||||||
|     "loginOauthSuccessTitle": "Redirecting", |     "loginOauthSuccessTitle": "Redirecting", | ||||||
|     "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", |     "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...", |     "continueRedirectingTitle": "Redirecting...", | ||||||
|     "continueRedirectingSubtitle": "You should be redirected to the app soon", |     "continueRedirectingSubtitle": "You should be redirected to the app soon", | ||||||
|     "continueInvalidRedirectTitle": "Invalid redirect", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", |  | ||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueInsecureRedirectTitle": "Insecure redirect", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", | ||||||
|     "continueTitle": "Continue", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueSubtitle": "Click the button to continue to your app.", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "Failed to log out", |     "logoutFailTitle": "Failed to log out", | ||||||
|     "logoutFailSubtitle": "Please try again", |     "logoutFailSubtitle": "Please try again", | ||||||
|     "logoutSuccessTitle": "Logged out", |     "logoutSuccessTitle": "Logged out", | ||||||
| @@ -42,12 +45,18 @@ | |||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||||
|  |     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |  | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |  | ||||||
|     "cancelTitle": "Cancel", |     "cancelTitle": "Cancel", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "forgotPasswordTitle": "Forgot your password?", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||||
|     "errorTitle": "An error occurred", |     "errorTitle": "An error occurred", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "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", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -14,14 +14,17 @@ | |||||||
|     "loginOauthFailSubtitle": "Failed to get OAuth URL", |     "loginOauthFailSubtitle": "Failed to get OAuth URL", | ||||||
|     "loginOauthSuccessTitle": "Redirecting", |     "loginOauthSuccessTitle": "Redirecting", | ||||||
|     "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", |     "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...", |     "continueRedirectingTitle": "Redirecting...", | ||||||
|     "continueRedirectingSubtitle": "You should be redirected to the app soon", |     "continueRedirectingSubtitle": "You should be redirected to the app soon", | ||||||
|     "continueInvalidRedirectTitle": "Invalid redirect", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", |  | ||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueInsecureRedirectTitle": "Insecure redirect", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", | ||||||
|     "continueTitle": "Continue", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueSubtitle": "Click the button to continue to your app.", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "Failed to log out", |     "logoutFailTitle": "Failed to log out", | ||||||
|     "logoutFailSubtitle": "Please try again", |     "logoutFailSubtitle": "Please try again", | ||||||
|     "logoutSuccessTitle": "Logged out", |     "logoutSuccessTitle": "Logged out", | ||||||
| @@ -42,12 +45,18 @@ | |||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||||
|  |     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |  | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |  | ||||||
|     "cancelTitle": "Cancel", |     "cancelTitle": "Cancel", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "forgotPasswordTitle": "Forgot your password?", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||||
|     "errorTitle": "An error occurred", |     "errorTitle": "An error occurred", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "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", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -14,14 +14,17 @@ | |||||||
|     "loginOauthFailSubtitle": "Fout bij het ophalen van OAuth URL", |     "loginOauthFailSubtitle": "Fout bij het ophalen van OAuth URL", | ||||||
|     "loginOauthSuccessTitle": "Omleiden", |     "loginOauthSuccessTitle": "Omleiden", | ||||||
|     "loginOauthSuccessSubtitle": "Omleiden naar je OAuth provider", |     "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...", |     "continueRedirectingTitle": "Omleiden...", | ||||||
|     "continueRedirectingSubtitle": "Je wordt naar de app doorgestuurd", |     "continueRedirectingSubtitle": "Je wordt naar de app doorgestuurd", | ||||||
|     "continueInvalidRedirectTitle": "Ongeldige omleiding", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueInvalidRedirectSubtitle": "De omleidings-URL is ongeldig", |  | ||||||
|     "continueInsecureRedirectTitle": "Onveilige doorverwijzing", |     "continueInsecureRedirectTitle": "Onveilige doorverwijzing", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", | ||||||
|     "continueTitle": "Ga verder", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueSubtitle": "Klik op de knop om door te gaan naar de app.", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "Afmelden mislukt", |     "logoutFailTitle": "Afmelden mislukt", | ||||||
|     "logoutFailSubtitle": "Probeer het opnieuw", |     "logoutFailSubtitle": "Probeer het opnieuw", | ||||||
|     "logoutSuccessTitle": "Afgemeld", |     "logoutSuccessTitle": "Afgemeld", | ||||||
| @@ -42,12 +45,18 @@ | |||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||||
|  |     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Opnieuw proberen", |     "unauthorizedButton": "Opnieuw proberen", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |  | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |  | ||||||
|     "cancelTitle": "Cancel", |     "cancelTitle": "Cancel", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "forgotPasswordTitle": "Forgot your password?", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||||
|     "errorTitle": "An error occurred", |     "errorTitle": "An error occurred", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "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", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -14,14 +14,17 @@ | |||||||
|     "loginOauthFailSubtitle": "Failed to get OAuth URL", |     "loginOauthFailSubtitle": "Failed to get OAuth URL", | ||||||
|     "loginOauthSuccessTitle": "Redirecting", |     "loginOauthSuccessTitle": "Redirecting", | ||||||
|     "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", |     "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...", |     "continueRedirectingTitle": "Redirecting...", | ||||||
|     "continueRedirectingSubtitle": "You should be redirected to the app soon", |     "continueRedirectingSubtitle": "You should be redirected to the app soon", | ||||||
|     "continueInvalidRedirectTitle": "Invalid redirect", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", |  | ||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueInsecureRedirectTitle": "Insecure redirect", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", | ||||||
|     "continueTitle": "Continue", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueSubtitle": "Click the button to continue to your app.", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "Failed to log out", |     "logoutFailTitle": "Failed to log out", | ||||||
|     "logoutFailSubtitle": "Please try again", |     "logoutFailSubtitle": "Please try again", | ||||||
|     "logoutSuccessTitle": "Logged out", |     "logoutSuccessTitle": "Logged out", | ||||||
| @@ -42,12 +45,18 @@ | |||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||||
|  |     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |  | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |  | ||||||
|     "cancelTitle": "Cancel", |     "cancelTitle": "Cancel", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "forgotPasswordTitle": "Forgot your password?", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||||
|     "errorTitle": "An error occurred", |     "errorTitle": "An error occurred", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "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", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
|     "loginTitle": "Witaj ponownie, zaloguj się przez", |     "loginTitle": "Witaj ponownie, zaloguj się przez", | ||||||
|     "loginTitleSimple": "Witaj ponownie, zaloguj się", |     "loginTitleSimple": "Witaj ponownie, zaloguj się", | ||||||
|     "loginDivider": "lub", |     "loginDivider": "Lub", | ||||||
|     "loginUsername": "Nazwa użytkownika", |     "loginUsername": "Nazwa użytkownika", | ||||||
|     "loginPassword": "Hasło", |     "loginPassword": "Hasło", | ||||||
|     "loginSubmit": "Zaloguj się", |     "loginSubmit": "Zaloguj się", | ||||||
| @@ -14,19 +14,22 @@ | |||||||
|     "loginOauthFailSubtitle": "Nie udało się uzyskać adresu URL OAuth", |     "loginOauthFailSubtitle": "Nie udało się uzyskać adresu URL OAuth", | ||||||
|     "loginOauthSuccessTitle": "Przekierowywanie", |     "loginOauthSuccessTitle": "Przekierowywanie", | ||||||
|     "loginOauthSuccessSubtitle": "Przekierowywanie do Twojego dostawcy OAuth", |     "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...", |     "continueRedirectingTitle": "Przekierowywanie...", | ||||||
|     "continueRedirectingSubtitle": "Wkrótce powinieneś zostać przekierowany do aplikacji", |     "continueRedirectingSubtitle": "Wkrótce powinieneś zostać przekierowany do aplikacji", | ||||||
|     "continueInvalidRedirectTitle": "Nieprawidłowe przekierowanie", |     "continueRedirectManually": "Przekieruj mnie ręcznie", | ||||||
|     "continueInvalidRedirectSubtitle": "Adres przekierowania jest nieprawidłowy", |  | ||||||
|     "continueInsecureRedirectTitle": "Niezabezpieczone przekierowanie", |     "continueInsecureRedirectTitle": "Niezabezpieczone przekierowanie", | ||||||
|     "continueInsecureRedirectSubtitle": "Próbujesz przekierować z <code>https</code> do <code>http</code>, co nie jest bezpieczne. Czy na pewno chcesz kontynuować?", |     "continueInsecureRedirectSubtitle": "Próbujesz przekierować z <code>https</code> do <code>http</code>, co nie jest bezpieczne. Czy na pewno chcesz kontynuować?", | ||||||
|     "continueTitle": "Kontynuuj", |     "continueUntrustedRedirectTitle": "Niezaufane przekierowanie", | ||||||
|     "continueSubtitle": "Kliknij przycisk, aby przejść do aplikacji.", |     "continueUntrustedRedirectSubtitle": "Próbujesz przekierować do domeny, która nie pasuje do skonfigurowanej domeny (<code>{{cookieDomain}}</code>). Czy na pewno chcesz kontynuować?", | ||||||
|     "logoutFailTitle": "Nie udało się wylogować", |     "logoutFailTitle": "Nie udało się wylogować", | ||||||
|     "logoutFailSubtitle": "Spróbuj ponownie", |     "logoutFailSubtitle": "Spróbuj ponownie", | ||||||
|     "logoutSuccessTitle": "Wylogowano", |     "logoutSuccessTitle": "Wylogowano", | ||||||
|     "logoutSuccessSubtitle": "Zostałeś wylogowany", |     "logoutSuccessSubtitle": "Zostałeś wylogowany", | ||||||
|     "logoutTitle": "Wylogowanie", |     "logoutTitle": "Wyloguj się", | ||||||
|     "logoutUsernameSubtitle": "Jesteś obecnie zalogowany jako <code>{{username}}</code>. Kliknij poniższy przycisk, aby się wylogować.", |     "logoutUsernameSubtitle": "Jesteś obecnie zalogowany jako <code>{{username}}</code>. Kliknij poniższy przycisk, aby się wylogować.", | ||||||
|     "logoutOauthSubtitle": "Obecnie jesteś zalogowany jako <code>{{username}}</code> przy użyciu dostawcy {{provider}} OAuth. Kliknij poniższy przycisk, aby się wylogować.", |     "logoutOauthSubtitle": "Obecnie jesteś zalogowany jako <code>{{username}}</code> przy użyciu dostawcy {{provider}} OAuth. Kliknij poniższy przycisk, aby się wylogować.", | ||||||
|     "notFoundTitle": "Nie znaleziono strony", |     "notFoundTitle": "Nie znaleziono strony", | ||||||
| @@ -42,12 +45,18 @@ | |||||||
|     "unauthorizedResourceSubtitle": "Użytkownik o nazwie użytkownika <code>{{username}}</code> nie ma uprawnień dostępu do zasobu <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "Użytkownik o nazwie użytkownika <code>{{username}}</code> nie ma uprawnień dostępu do zasobu <code>{{resource}}</code>.", | ||||||
|     "unauthorizedLoginSubtitle": "Użytkownik o nazwie <code>{{username}}</code> nie jest upoważniony do zalogowania się.", |     "unauthorizedLoginSubtitle": "Użytkownik o nazwie <code>{{username}}</code> nie jest upoważniony do zalogowania się.", | ||||||
|     "unauthorizedGroupsSubtitle": "Użytkownik o nazwie <code>{{username}}</code> nie należy do grup wymaganych przez zasób <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "Użytkownik o nazwie <code>{{username}}</code> nie należy do grup wymaganych przez zasób <code>{{resource}}</code>.", | ||||||
|  |     "unauthorizedIpSubtitle": "Twój adres IP <code>{{ip}}</code> nie ma autoryzacji do dostępu do zasobu <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Spróbuj ponownie", |     "unauthorizedButton": "Spróbuj ponownie", | ||||||
|     "untrustedRedirectTitle": "Niezaufane przekierowanie", |  | ||||||
|     "untrustedRedirectSubtitle": "Próbujesz przekierować do domeny, która nie pasuje do Twojej skonfigurowanej domeny (<code>{{domain}}</code>). Czy na pewno chcesz kontynuować?", |  | ||||||
|     "cancelTitle": "Anuluj", |     "cancelTitle": "Anuluj", | ||||||
|     "forgotPasswordTitle": "Nie pamiętasz hasła?", |     "forgotPasswordTitle": "Nie pamiętasz hasła?", | ||||||
|     "failedToFetchProvidersTitle": "Nie udało się załadować dostawców uwierzytelniania. Sprawdź swoją konfigurację.", |     "failedToFetchProvidersTitle": "Nie udało się załadować dostawców uwierzytelniania. Sprawdź swoją konfigurację.", | ||||||
|     "errorTitle": "Wystąpił błąd", |     "errorTitle": "Wystąpił błąd", | ||||||
|     "errorSubtitle": "Wystąpił błąd podczas próby wykonania tej czynności. Sprawdź konsolę, aby uzyskać więcej informacji." |     "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", | ||||||
|  |     "domainWarningTitle": "Nieprawidłowa domena", | ||||||
|  |     "domainWarningSubtitle": "Ta instancja jest skonfigurowana do uzyskania dostępu z <code>{{appUrl}}</code>, ale <code>{{currentUrl}}</code> jest w użyciu. Jeśli będziesz kontynuować, mogą wystąpić problemy z uwierzytelnianiem.", | ||||||
|  |     "ignoreTitle": "Zignoruj", | ||||||
|  |     "goToCorrectDomainTitle": "Przejdź do prawidłowej domeny" | ||||||
| } | } | ||||||
| @@ -14,14 +14,17 @@ | |||||||
|     "loginOauthFailSubtitle": "Falha ao obter URL de OAuth", |     "loginOauthFailSubtitle": "Falha ao obter URL de OAuth", | ||||||
|     "loginOauthSuccessTitle": "Redirecionando", |     "loginOauthSuccessTitle": "Redirecionando", | ||||||
|     "loginOauthSuccessSubtitle": "Redirecionando para seu provedor OAuth", |     "loginOauthSuccessSubtitle": "Redirecionando para seu provedor OAuth", | ||||||
|  |     "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", | ||||||
|  |     "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", | ||||||
|  |     "loginOauthAutoRedirectButton": "Redirect now", | ||||||
|  |     "continueTitle": "Continuar", | ||||||
|     "continueRedirectingTitle": "Redirecionando...", |     "continueRedirectingTitle": "Redirecionando...", | ||||||
|     "continueRedirectingSubtitle": "Você deve ser redirecionado para o aplicativo em breve", |     "continueRedirectingSubtitle": "Você deve ser redirecionado para o aplicativo em breve", | ||||||
|     "continueInvalidRedirectTitle": "Redirecionamento inválido", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueInvalidRedirectSubtitle": "O endereço de redirecionamento é inválido", |  | ||||||
|     "continueInsecureRedirectTitle": "Redirecionamento inseguro", |     "continueInsecureRedirectTitle": "Redirecionamento inseguro", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", | ||||||
|     "continueTitle": "Continuar", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueSubtitle": "Clique no botão para continuar para o seu aplicativo.", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "Falha ao encerrar sessão", |     "logoutFailTitle": "Falha ao encerrar sessão", | ||||||
|     "logoutFailSubtitle": "Por favor, tente novamente", |     "logoutFailSubtitle": "Por favor, tente novamente", | ||||||
|     "logoutSuccessTitle": "Sessão encerrada", |     "logoutSuccessTitle": "Sessão encerrada", | ||||||
| @@ -42,12 +45,18 @@ | |||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||||
|  |     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Tentar novamente", |     "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 (<code>{{domain}}</code>). Are you sure you want to continue?", |  | ||||||
|     "cancelTitle": "Cancelar", |     "cancelTitle": "Cancelar", | ||||||
|     "forgotPasswordTitle": "Esqueceu sua senha?", |     "forgotPasswordTitle": "Esqueceu sua senha?", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||||
|     "errorTitle": "An error occurred", |     "errorTitle": "An error occurred", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "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", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -14,14 +14,17 @@ | |||||||
|     "loginOauthFailSubtitle": "Failed to get OAuth URL", |     "loginOauthFailSubtitle": "Failed to get OAuth URL", | ||||||
|     "loginOauthSuccessTitle": "Redirecting", |     "loginOauthSuccessTitle": "Redirecting", | ||||||
|     "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", |     "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...", |     "continueRedirectingTitle": "Redirecting...", | ||||||
|     "continueRedirectingSubtitle": "You should be redirected to the app soon", |     "continueRedirectingSubtitle": "You should be redirected to the app soon", | ||||||
|     "continueInvalidRedirectTitle": "Invalid redirect", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", |  | ||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueInsecureRedirectTitle": "Insecure redirect", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", | ||||||
|     "continueTitle": "Continue", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueSubtitle": "Click the button to continue to your app.", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "Failed to log out", |     "logoutFailTitle": "Failed to log out", | ||||||
|     "logoutFailSubtitle": "Please try again", |     "logoutFailSubtitle": "Please try again", | ||||||
|     "logoutSuccessTitle": "Logged out", |     "logoutSuccessTitle": "Logged out", | ||||||
| @@ -42,12 +45,18 @@ | |||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||||
|  |     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |  | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |  | ||||||
|     "cancelTitle": "Cancel", |     "cancelTitle": "Cancel", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "forgotPasswordTitle": "Forgot your password?", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||||
|     "errorTitle": "An error occurred", |     "errorTitle": "An error occurred", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "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", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -14,14 +14,17 @@ | |||||||
|     "loginOauthFailSubtitle": "Failed to get OAuth URL", |     "loginOauthFailSubtitle": "Failed to get OAuth URL", | ||||||
|     "loginOauthSuccessTitle": "Redirecting", |     "loginOauthSuccessTitle": "Redirecting", | ||||||
|     "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", |     "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...", |     "continueRedirectingTitle": "Redirecting...", | ||||||
|     "continueRedirectingSubtitle": "You should be redirected to the app soon", |     "continueRedirectingSubtitle": "You should be redirected to the app soon", | ||||||
|     "continueInvalidRedirectTitle": "Invalid redirect", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", |  | ||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueInsecureRedirectTitle": "Insecure redirect", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", | ||||||
|     "continueTitle": "Continue", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueSubtitle": "Click the button to continue to your app.", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "Failed to log out", |     "logoutFailTitle": "Failed to log out", | ||||||
|     "logoutFailSubtitle": "Please try again", |     "logoutFailSubtitle": "Please try again", | ||||||
|     "logoutSuccessTitle": "Logged out", |     "logoutSuccessTitle": "Logged out", | ||||||
| @@ -42,12 +45,18 @@ | |||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||||
|  |     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |  | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |  | ||||||
|     "cancelTitle": "Cancel", |     "cancelTitle": "Cancel", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "forgotPasswordTitle": "Forgot your password?", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||||
|     "errorTitle": "An error occurred", |     "errorTitle": "An error occurred", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "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", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|     "loginTitle": "С возвращением, войти с", |     "loginTitle": "С возвращением, войти с", | ||||||
|     "loginTitleSimple": "Вход", |     "loginTitleSimple": "С возвращением, пожалуйста войдите", | ||||||
|     "loginDivider": "Или", |     "loginDivider": "Или", | ||||||
|     "loginUsername": "Имя пользователя", |     "loginUsername": "Имя пользователя", | ||||||
|     "loginPassword": "Пароль", |     "loginPassword": "Пароль", | ||||||
| @@ -8,20 +8,23 @@ | |||||||
|     "loginFailTitle": "Вход не удался", |     "loginFailTitle": "Вход не удался", | ||||||
|     "loginFailSubtitle": "Проверьте имя пользователя и пароль", |     "loginFailSubtitle": "Проверьте имя пользователя и пароль", | ||||||
|     "loginFailRateLimit": "Слишком много ошибок входа. Попробуйте позже", |     "loginFailRateLimit": "Слишком много ошибок входа. Попробуйте позже", | ||||||
|     "loginSuccessTitle": "Вы вошли", |     "loginSuccessTitle": "Вход выполнен", | ||||||
|     "loginSuccessSubtitle": "С возвращением!", |     "loginSuccessSubtitle": "С возвращением!", | ||||||
|     "loginOauthFailTitle": "Произошла ошибка", |     "loginOauthFailTitle": "Произошла ошибка", | ||||||
|     "loginOauthFailSubtitle": "Не удалось получить OAuth URL", |     "loginOauthFailSubtitle": "Не удалось получить OAuth URL", | ||||||
|     "loginOauthSuccessTitle": "Перенаправление", |     "loginOauthSuccessTitle": "Перенаправление", | ||||||
|     "loginOauthSuccessSubtitle": "Перенаправление к поставщику OAuth", |     "loginOauthSuccessSubtitle": "Перенаправление к поставщику OAuth", | ||||||
|  |     "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", | ||||||
|  |     "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", | ||||||
|  |     "loginOauthAutoRedirectButton": "Redirect now", | ||||||
|  |     "continueTitle": "Продолжить", | ||||||
|     "continueRedirectingTitle": "Перенаправление...", |     "continueRedirectingTitle": "Перенаправление...", | ||||||
|     "continueRedirectingSubtitle": "Скоро вы будете перенаправлены в приложение", |     "continueRedirectingSubtitle": "Скоро вы будете перенаправлены в приложение", | ||||||
|     "continueInvalidRedirectTitle": "Неверное перенаправление", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueInvalidRedirectSubtitle": "URL перенаправления недействителен", |  | ||||||
|     "continueInsecureRedirectTitle": "Небезопасное перенаправление", |     "continueInsecureRedirectTitle": "Небезопасное перенаправление", | ||||||
|     "continueInsecureRedirectSubtitle": "Попытка перенаправления с <code>https</code> на <code>http</code>, уверены, что хотите продолжить?", |     "continueInsecureRedirectSubtitle": "Попытка перенаправления с <code>https</code> на <code>http</code>, уверены, что хотите продолжить?", | ||||||
|     "continueTitle": "Продолжить", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueSubtitle": "Нажмите на кнопку, чтобы перейти к приложению.", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "Не удалось выйти", |     "logoutFailTitle": "Не удалось выйти", | ||||||
|     "logoutFailSubtitle": "Попробуйте ещё раз", |     "logoutFailSubtitle": "Попробуйте ещё раз", | ||||||
|     "logoutSuccessTitle": "Выход", |     "logoutSuccessTitle": "Выход", | ||||||
| @@ -37,17 +40,23 @@ | |||||||
|     "totpSuccessTitle": "Подтверждён", |     "totpSuccessTitle": "Подтверждён", | ||||||
|     "totpSuccessSubtitle": "Перенаправление в приложение", |     "totpSuccessSubtitle": "Перенаправление в приложение", | ||||||
|     "totpTitle": "Введите код TOTP", |     "totpTitle": "Введите код TOTP", | ||||||
|     "totpSubtitle": "Пожалуйста, введите код из вашего приложения — аутентификатора.", |     "totpSubtitle": "Пожалуйста, введите код из вашего приложения-аутентификатора.", | ||||||
|     "unauthorizedTitle": "Доступ запрещен", |     "unauthorizedTitle": "Доступ запрещен", | ||||||
|     "unauthorizedResourceSubtitle": "Пользователю <code>{{username}}</code> не разрешен доступ к <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "Пользователю <code>{{username}}</code> не разрешен доступ к <code>{{resource}}</code>.", | ||||||
|     "unauthorizedLoginSubtitle": "Пользователю <code>{{username}}</code> не разрешен вход.", |     "unauthorizedLoginSubtitle": "Пользователю <code>{{username}}</code> не разрешен вход.", | ||||||
|     "unauthorizedGroupsSubtitle": "Пользователь <code>{{username}}</code> не состоит в группах, которым разрешен доступ к <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "Пользователь <code>{{username}}</code> не состоит в группах, которым разрешен доступ к <code>{{resource}}</code>.", | ||||||
|  |     "unauthorizedIpSubtitle": "Ваш IP адрес <code>{{ip}}</code> не авторизован для доступа к ресурсу <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Повторить", |     "unauthorizedButton": "Повторить", | ||||||
|     "untrustedRedirectTitle": "Ненадежное перенаправление", |  | ||||||
|     "untrustedRedirectSubtitle": "Попытка перенаправить на домен, который не соответствует вашему заданному домену (<code>{{domain}}</code>). Уверены, что хотите продолжить?", |  | ||||||
|     "cancelTitle": "Отмена", |     "cancelTitle": "Отмена", | ||||||
|     "forgotPasswordTitle": "Забыли пароль?", |     "forgotPasswordTitle": "Забыли пароль?", | ||||||
|     "failedToFetchProvidersTitle": "Не удалось загрузить провайдеров аутентификации. Пожалуйста, проверьте конфигурацию.", |     "failedToFetchProvidersTitle": "Не удалось загрузить провайдеров аутентификации. Пожалуйста, проверьте конфигурацию.", | ||||||
|     "errorTitle": "Произошла ошибка", |     "errorTitle": "Произошла ошибка", | ||||||
|     "errorSubtitle": "Произошла ошибка при попытке выполнить это действие. Проверьте консоль для дополнительной информации." |     "errorSubtitle": "Произошла ошибка при попытке выполнить это действие. Проверьте консоль для дополнительной информации.", | ||||||
|  |     "forgotPasswordMessage": "Вы можете сбросить свой пароль, изменив переменную окружения `USERS`.", | ||||||
|  |     "fieldRequired": "Это поле является обязательным", | ||||||
|  |     "invalidInput": "Недопустимый ввод", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -1,53 +1,62 @@ | |||||||
| { | { | ||||||
|     "loginTitle": "Welcome back, login with", |     "loginTitle": "Добродошли назад, пријавите се са", | ||||||
|     "loginTitleSimple": "Welcome back, please login", |     "loginTitleSimple": "Добродошли назад, молим вас пријавите се", | ||||||
|     "loginDivider": "Or", |     "loginDivider": "Или", | ||||||
|     "loginUsername": "Username", |     "loginUsername": "Корисничко име", | ||||||
|     "loginPassword": "Password", |     "loginPassword": "Лозинка", | ||||||
|     "loginSubmit": "Login", |     "loginSubmit": "Пријава", | ||||||
|     "loginFailTitle": "Failed to log in", |     "loginFailTitle": "Неуспешна пријава", | ||||||
|     "loginFailSubtitle": "Please check your username and password", |     "loginFailSubtitle": "Молим вас проверите ваше корисничко име и лозинку", | ||||||
|     "loginFailRateLimit": "You failed to login too many times. Please try again later", |     "loginFailRateLimit": "Нисте успели да се пријавите превише пута. Молим вас покушајте касније", | ||||||
|     "loginSuccessTitle": "Logged in", |     "loginSuccessTitle": "Пријављени", | ||||||
|     "loginSuccessSubtitle": "Welcome back!", |     "loginSuccessSubtitle": "Добродошли назад!", | ||||||
|     "loginOauthFailTitle": "An error occurred", |     "loginOauthFailTitle": "Појавила се грешка", | ||||||
|     "loginOauthFailSubtitle": "Failed to get OAuth URL", |     "loginOauthFailSubtitle": "Неуспело преузимање OAuth адресе", | ||||||
|     "loginOauthSuccessTitle": "Redirecting", |     "loginOauthSuccessTitle": "Преусмеравање", | ||||||
|     "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", |     "loginOauthSuccessSubtitle": "Преусмеравање на вашег OAuth провајдера", | ||||||
|     "continueRedirectingTitle": "Redirecting...", |     "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", | ||||||
|     "continueRedirectingSubtitle": "You should be redirected to the app soon", |     "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", | ||||||
|     "continueInvalidRedirectTitle": "Invalid redirect", |     "loginOauthAutoRedirectButton": "Redirect now", | ||||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", |     "continueTitle": "Настави", | ||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueRedirectingTitle": "Преусмеравање...", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueRedirectingSubtitle": "Требали би сте ускоро да будете преусмерени на апликацију", | ||||||
|     "continueTitle": "Continue", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueSubtitle": "Click the button to continue to your app.", |     "continueInsecureRedirectTitle": "Небезбедно преусмеравање", | ||||||
|     "logoutFailTitle": "Failed to log out", |     "continueInsecureRedirectSubtitle": "Покушавате да преусмерите са <code>https</code> на <code>http</code> што није безбедно. Да ли желите да наставите?", | ||||||
|     "logoutFailSubtitle": "Please try again", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "logoutSuccessTitle": "Logged out", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutSuccessSubtitle": "You have been logged out", |     "logoutFailTitle": "Неуспешно одјављивање", | ||||||
|     "logoutTitle": "Logout", |     "logoutFailSubtitle": "Молим вас покушајте поново", | ||||||
|     "logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.", |     "logoutSuccessTitle": "Одјављени", | ||||||
|     "logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.", |     "logoutSuccessSubtitle": "Одјављени сте", | ||||||
|     "notFoundTitle": "Page not found", |     "logoutTitle": "Одјава", | ||||||
|     "notFoundSubtitle": "The page you are looking for does not exist.", |     "logoutUsernameSubtitle": "Тренутно сте пријављени као <code>{{username}}</code>. Кликните на дугме испод да се одјавите.", | ||||||
|     "notFoundButton": "Go home", |     "logoutOauthSubtitle": "Тренутно сте пријављени као <code>{{username}}</code> користећи {{provider}} OAuth провајдера. Кликните на дугме испод да се одјавите.", | ||||||
|     "totpFailTitle": "Failed to verify code", |     "notFoundTitle": "Страница није пронађена", | ||||||
|     "totpFailSubtitle": "Please check your code and try again", |     "notFoundSubtitle": "Страница коју тражите не постоји.", | ||||||
|     "totpSuccessTitle": "Verified", |     "notFoundButton": "На почетак", | ||||||
|     "totpSuccessSubtitle": "Redirecting to your app", |     "totpFailTitle": "Неуспело потврђивање кода", | ||||||
|     "totpTitle": "Enter your TOTP code", |     "totpFailSubtitle": "Молим вас проверите ваш код и покушајте поново", | ||||||
|     "totpSubtitle": "Please enter the code from your authenticator app.", |     "totpSuccessTitle": "Потврђен", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "totpSuccessSubtitle": "Преусмеравање на вашу апликацију", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "totpTitle": "Унесите ваш TOTP код", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "totpSubtitle": "Молим вас унесите код из ваше апликације за аутентификацију.", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedTitle": "Неауторизован", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedResourceSubtitle": "Корисник са корисничким именом <code>{{username}}</code> није ауторизован да приступи ресурсу <code>{{resource}}</code>.", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "unauthorizedLoginSubtitle": "Корисник са корисничким именом <code>{{username}}</code> није ауторизован за пријављивање.", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |     "unauthorizedGroupsSubtitle": "Корисник са корисничким именом <code>{{username}}</code> није у групама које захтева ресурс <code>{{resource}}</code>.", | ||||||
|     "cancelTitle": "Cancel", |     "unauthorizedIpSubtitle": "Ваша IP адреса <code>{{ip}}</code> није ауторизована да приступи ресурсу <code>{{resource}}</code>.", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "unauthorizedButton": "Покушајте поново", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "cancelTitle": "Поништи", | ||||||
|     "errorTitle": "An error occurred", |     "forgotPasswordTitle": "Заборавили сте лозинку?", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "failedToFetchProvidersTitle": "Није успело учитавање провајдера аутентификације. Молим вас проверите ваша подешавања.", | ||||||
|  |     "errorTitle": "Појавила се грешка", | ||||||
|  |     "errorSubtitle": "Појавила се грешка при покушају извршавања ове радње. Молим вас проверите конзолу за додатне информације.", | ||||||
|  |     "forgotPasswordMessage": "Можете поништити вашу лозинку променом `USERS` променљиве окружења.", | ||||||
|  |     "fieldRequired": "This field is required", | ||||||
|  |     "invalidInput": "Invalid input", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -1,32 +1,35 @@ | |||||||
| { | { | ||||||
|     "loginTitle": "Welcome back, login with", |     "loginTitle": "Välkommen tillbaka, logga in med", | ||||||
|     "loginTitleSimple": "Welcome back, please login", |     "loginTitleSimple": "Välkommen tillbaka, logga in", | ||||||
|     "loginDivider": "Or", |     "loginDivider": "Eller", | ||||||
|     "loginUsername": "Username", |     "loginUsername": "Användarnamn", | ||||||
|     "loginPassword": "Password", |     "loginPassword": "Lösenord", | ||||||
|     "loginSubmit": "Login", |     "loginSubmit": "Logga in", | ||||||
|     "loginFailTitle": "Failed to log in", |     "loginFailTitle": "Kunde inte logga in", | ||||||
|     "loginFailSubtitle": "Please check your username and password", |     "loginFailSubtitle": "Kontrollera ditt användarnamn och lösenord", | ||||||
|     "loginFailRateLimit": "You failed to login too many times. Please try again later", |     "loginFailRateLimit": "Du misslyckades med att logga in för många gånger. Försök igen senare", | ||||||
|     "loginSuccessTitle": "Logged in", |     "loginSuccessTitle": "Inloggad", | ||||||
|     "loginSuccessSubtitle": "Welcome back!", |     "loginSuccessSubtitle": "Välkommen tillbaka!", | ||||||
|     "loginOauthFailTitle": "An error occurred", |     "loginOauthFailTitle": "Ett fel har uppstått", | ||||||
|     "loginOauthFailSubtitle": "Failed to get OAuth URL", |     "loginOauthFailSubtitle": "Kunde inte hämta OAuth URL", | ||||||
|     "loginOauthSuccessTitle": "Redirecting", |     "loginOauthSuccessTitle": "Omdirigerar", | ||||||
|     "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", |     "loginOauthSuccessSubtitle": "Omdirigera till din OAuth leverantör", | ||||||
|     "continueRedirectingTitle": "Redirecting...", |     "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", | ||||||
|     "continueRedirectingSubtitle": "You should be redirected to the app soon", |     "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", | ||||||
|     "continueInvalidRedirectTitle": "Invalid redirect", |     "loginOauthAutoRedirectButton": "Redirect now", | ||||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", |     "continueTitle": "Fortsätt", | ||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueRedirectingTitle": "Omdirigerar...", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueRedirectingSubtitle": "Du bör omdirigeras till appen snart", | ||||||
|     "continueTitle": "Continue", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueSubtitle": "Click the button to continue to your app.", |     "continueInsecureRedirectTitle": "Osäker omdirigering", | ||||||
|     "logoutFailTitle": "Failed to log out", |     "continueInsecureRedirectSubtitle": "Du försöker omdirigera från <code>https</code> till <code>http</code> som inte är säker. Är du säker på att du vill fortsätta?", | ||||||
|     "logoutFailSubtitle": "Please try again", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "logoutSuccessTitle": "Logged out", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutSuccessSubtitle": "You have been logged out", |     "logoutFailTitle": "Kunde inte logga ut.", | ||||||
|     "logoutTitle": "Logout", |     "logoutFailSubtitle": "Vänligen försök igen", | ||||||
|  |     "logoutSuccessTitle": "Utloggad", | ||||||
|  |     "logoutSuccessSubtitle": "Du har blivit utloggad", | ||||||
|  |     "logoutTitle": "Logga ut", | ||||||
|     "logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.", |     "logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.", | ||||||
|     "logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.", |     "logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.", | ||||||
|     "notFoundTitle": "Page not found", |     "notFoundTitle": "Page not found", | ||||||
| @@ -38,16 +41,22 @@ | |||||||
|     "totpSuccessSubtitle": "Redirecting to your app", |     "totpSuccessSubtitle": "Redirecting to your app", | ||||||
|     "totpTitle": "Enter your TOTP code", |     "totpTitle": "Enter your TOTP code", | ||||||
|     "totpSubtitle": "Please enter the code from your authenticator app.", |     "totpSubtitle": "Please enter the code from your authenticator app.", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "unauthorizedTitle": "Obehörig", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||||
|  |     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |  | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |  | ||||||
|     "cancelTitle": "Cancel", |     "cancelTitle": "Cancel", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "forgotPasswordTitle": "Forgot your password?", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||||
|     "errorTitle": "An error occurred", |     "errorTitle": "An error occurred", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "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", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -14,14 +14,17 @@ | |||||||
|     "loginOauthFailSubtitle": "Failed to get OAuth URL", |     "loginOauthFailSubtitle": "Failed to get OAuth URL", | ||||||
|     "loginOauthSuccessTitle": "Yönlendiriliyor", |     "loginOauthSuccessTitle": "Yönlendiriliyor", | ||||||
|     "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", |     "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...", |     "continueRedirectingTitle": "Yönlendiriliyor...", | ||||||
|     "continueRedirectingSubtitle": "You should be redirected to the app soon", |     "continueRedirectingSubtitle": "You should be redirected to the app soon", | ||||||
|     "continueInvalidRedirectTitle": "Invalid redirect", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", |  | ||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueInsecureRedirectTitle": "Insecure redirect", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", | ||||||
|     "continueTitle": "Devam et", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueSubtitle": "Click the button to continue to your app.", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "Failed to log out", |     "logoutFailTitle": "Failed to log out", | ||||||
|     "logoutFailSubtitle": "Lütfen tekrar deneyin", |     "logoutFailSubtitle": "Lütfen tekrar deneyin", | ||||||
|     "logoutSuccessTitle": "Çıkış yapıldı", |     "logoutSuccessTitle": "Çıkış yapıldı", | ||||||
| @@ -42,12 +45,18 @@ | |||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||||
|  |     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |  | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |  | ||||||
|     "cancelTitle": "İptal", |     "cancelTitle": "İptal", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "forgotPasswordTitle": "Forgot your password?", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||||
|     "errorTitle": "An error occurred", |     "errorTitle": "An error occurred", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "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", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| { | { | ||||||
|     "loginTitle": "Welcome back, login with", |     "loginTitle": "З поверненням, увійдіть через", | ||||||
|     "loginTitleSimple": "Welcome back, please login", |     "loginTitleSimple": "Welcome back, please login", | ||||||
|     "loginDivider": "Or", |     "loginDivider": "Or", | ||||||
|     "loginUsername": "Username", |     "loginUsername": "Username", | ||||||
| @@ -14,14 +14,17 @@ | |||||||
|     "loginOauthFailSubtitle": "Failed to get OAuth URL", |     "loginOauthFailSubtitle": "Failed to get OAuth URL", | ||||||
|     "loginOauthSuccessTitle": "Redirecting", |     "loginOauthSuccessTitle": "Redirecting", | ||||||
|     "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", |     "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...", |     "continueRedirectingTitle": "Redirecting...", | ||||||
|     "continueRedirectingSubtitle": "You should be redirected to the app soon", |     "continueRedirectingSubtitle": "You should be redirected to the app soon", | ||||||
|     "continueInvalidRedirectTitle": "Invalid redirect", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", |  | ||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueInsecureRedirectTitle": "Insecure redirect", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", | ||||||
|     "continueTitle": "Continue", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueSubtitle": "Click the button to continue to your app.", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "Failed to log out", |     "logoutFailTitle": "Failed to log out", | ||||||
|     "logoutFailSubtitle": "Please try again", |     "logoutFailSubtitle": "Please try again", | ||||||
|     "logoutSuccessTitle": "Logged out", |     "logoutSuccessTitle": "Logged out", | ||||||
| @@ -42,12 +45,18 @@ | |||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||||
|  |     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |  | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |  | ||||||
|     "cancelTitle": "Cancel", |     "cancelTitle": "Cancel", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "forgotPasswordTitle": "Forgot your password?", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||||
|     "errorTitle": "An error occurred", |     "errorTitle": "An error occurred", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "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", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -14,14 +14,17 @@ | |||||||
|     "loginOauthFailSubtitle": "Failed to get OAuth URL", |     "loginOauthFailSubtitle": "Failed to get OAuth URL", | ||||||
|     "loginOauthSuccessTitle": "Redirecting", |     "loginOauthSuccessTitle": "Redirecting", | ||||||
|     "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", |     "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...", |     "continueRedirectingTitle": "Redirecting...", | ||||||
|     "continueRedirectingSubtitle": "You should be redirected to the app soon", |     "continueRedirectingSubtitle": "You should be redirected to the app soon", | ||||||
|     "continueInvalidRedirectTitle": "Invalid redirect", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", |  | ||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueInsecureRedirectTitle": "Insecure redirect", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", | ||||||
|     "continueTitle": "Continue", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueSubtitle": "Click the button to continue to your app.", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "Failed to log out", |     "logoutFailTitle": "Failed to log out", | ||||||
|     "logoutFailSubtitle": "Please try again", |     "logoutFailSubtitle": "Please try again", | ||||||
|     "logoutSuccessTitle": "Logged out", |     "logoutSuccessTitle": "Logged out", | ||||||
| @@ -42,12 +45,18 @@ | |||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||||
|  |     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |  | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |  | ||||||
|     "cancelTitle": "Cancel", |     "cancelTitle": "Cancel", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "forgotPasswordTitle": "Forgot your password?", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||||
|     "errorTitle": "An error occurred", |     "errorTitle": "An error occurred", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "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", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -1,53 +1,62 @@ | |||||||
| { | { | ||||||
|     "loginTitle": "欢迎回来,请登录", |     "loginTitle": "欢迎回来,请使用以下方式登录", | ||||||
|     "loginTitleSimple": "Welcome back, please login", |     "loginTitleSimple": "欢迎回来,请登录", | ||||||
|     "loginDivider": "或", |     "loginDivider": "或", | ||||||
|     "loginUsername": "用户名", |     "loginUsername": "用户名", | ||||||
|     "loginPassword": "密码", |     "loginPassword": "密码", | ||||||
|     "loginSubmit": "登录", |     "loginSubmit": "登录", | ||||||
|     "loginFailTitle": "登录失败", |     "loginFailTitle": "登录失败", | ||||||
|     "loginFailSubtitle": "请检查您的用户名和密码", |     "loginFailSubtitle": "请检查您的用户名和密码", | ||||||
|     "loginFailRateLimit": "You failed to login too many times. Please try again later", |     "loginFailRateLimit": "您登录失败次数过多。请稍后再试", | ||||||
|     "loginSuccessTitle": "已登录", |     "loginSuccessTitle": "已登录", | ||||||
|     "loginSuccessSubtitle": "欢迎回来!", |     "loginSuccessSubtitle": "欢迎回来!", | ||||||
|     "loginOauthFailTitle": "An error occurred", |     "loginOauthFailTitle": "发生错误", | ||||||
|     "loginOauthFailSubtitle": "获取 OAuth URL 失败", |     "loginOauthFailSubtitle": "获取 OAuth URL 失败", | ||||||
|     "loginOauthSuccessTitle": "重定向中", |     "loginOauthSuccessTitle": "重定向中", | ||||||
|     "loginOauthSuccessSubtitle": "重定向到您的 OAuth 提供商", |     "loginOauthSuccessSubtitle": "重定向到您的 OAuth 提供商", | ||||||
|  |     "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", | ||||||
|  |     "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", | ||||||
|  |     "loginOauthAutoRedirectButton": "Redirect now", | ||||||
|  |     "continueTitle": "继续", | ||||||
|     "continueRedirectingTitle": "正在重定向……", |     "continueRedirectingTitle": "正在重定向……", | ||||||
|     "continueRedirectingSubtitle": "您应该很快被重定向到应用", |     "continueRedirectingSubtitle": "您应该很快被重定向到应用", | ||||||
|     "continueInvalidRedirectTitle": "无效的重定向", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueInvalidRedirectSubtitle": "重定向URL无效", |  | ||||||
|     "continueInsecureRedirectTitle": "不安全的重定向", |     "continueInsecureRedirectTitle": "不安全的重定向", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "您正在尝试从<code>https</code>重定向到<code>http</code>可能存在风险。您确定要继续吗?", | ||||||
|     "continueTitle": "继续", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueSubtitle": "点击按钮以继续您的应用。", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "注销失败", |     "logoutFailTitle": "注销失败", | ||||||
|     "logoutFailSubtitle": "请重试", |     "logoutFailSubtitle": "请重试", | ||||||
|     "logoutSuccessTitle": "已登出", |     "logoutSuccessTitle": "已登出", | ||||||
|     "logoutSuccessSubtitle": "您已登出", |     "logoutSuccessSubtitle": "您已登出", | ||||||
|     "logoutTitle": "登出", |     "logoutTitle": "登出", | ||||||
|     "logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.", |     "logoutUsernameSubtitle": "您当前登录用户为<code>{{username}}</code>。点击下方按钮注销。", | ||||||
|     "logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.", |     "logoutOauthSubtitle": "您当前以<code>{{username}}</code>登录,使用的是{{provider}} OAuth 提供商。点击下方按钮注销。", | ||||||
|     "notFoundTitle": "无法找到页面", |     "notFoundTitle": "无法找到页面", | ||||||
|     "notFoundSubtitle": "您正在查找的页面不存在。", |     "notFoundSubtitle": "您访问的页面不存在。", | ||||||
|     "notFoundButton": "回到主页", |     "notFoundButton": "回到主页", | ||||||
|     "totpFailTitle": "无法验证代码", |     "totpFailTitle": "无法验证代码", | ||||||
|     "totpFailSubtitle": "请检查您的代码并重试", |     "totpFailSubtitle": "请检查您的代码并重试", | ||||||
|     "totpSuccessTitle": "已验证", |     "totpSuccessTitle": "已验证", | ||||||
|     "totpSuccessSubtitle": "重定向到您的应用", |     "totpSuccessSubtitle": "重定向到您的应用", | ||||||
|     "totpTitle": "输入您的 TOTP 代码", |     "totpTitle": "输入您的 TOTP 代码", | ||||||
|     "totpSubtitle": "Please enter the code from your authenticator app.", |     "totpSubtitle": "请输入您身份验证器应用中的代码。", | ||||||
|     "unauthorizedTitle": "未授权", |     "unauthorizedTitle": "未授权", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "unauthorizedResourceSubtitle": "用户名为<code>{{username}}</code>的用户无权访问资源<code>{{resource}}</code>。", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "用户名为<code>{{username}}</code>的用户无权登录。", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "用户名为<code>{{username}}</code>的用户不在资源<code>{{resource}}</code>所需的组中。", | ||||||
|  |     "unauthorizedIpSubtitle": "用户 <code>{{ip}}</code> 无权访问资源 <code>{{resource}}</code>。", | ||||||
|     "unauthorizedButton": "重试", |     "unauthorizedButton": "重试", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "cancelTitle": "取消", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |     "forgotPasswordTitle": "忘记密码?", | ||||||
|     "cancelTitle": "Cancel", |     "failedToFetchProvidersTitle": "加载身份验证提供程序失败,请检查您的配置。", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "errorTitle": "发生了错误", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "errorSubtitle": "执行此操作时发生错误,请检查控制台以获取更多信息。", | ||||||
|     "errorTitle": "An error occurred", |     "forgotPasswordMessage": "您可以通过更改 `USERS ` 环境变量重置您的密码。", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "fieldRequired": "必添字段", | ||||||
|  |     "invalidInput": "无效的输入", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -1,53 +1,62 @@ | |||||||
| { | { | ||||||
|     "loginTitle": "Welcome back, login with", |     "loginTitle": "歡迎回來,請用以下方式登入", | ||||||
|     "loginTitleSimple": "Welcome back, please login", |     "loginTitleSimple": "歡迎回來,請登入", | ||||||
|     "loginDivider": "Or", |     "loginDivider": "或", | ||||||
|     "loginUsername": "Username", |     "loginUsername": "帳號", | ||||||
|     "loginPassword": "Password", |     "loginPassword": "密碼", | ||||||
|     "loginSubmit": "Login", |     "loginSubmit": "登入", | ||||||
|     "loginFailTitle": "Failed to log in", |     "loginFailTitle": "登入失敗", | ||||||
|     "loginFailSubtitle": "Please check your username and password", |     "loginFailSubtitle": "請檢查您的帳號與密碼", | ||||||
|     "loginFailRateLimit": "You failed to login too many times. Please try again later", |     "loginFailRateLimit": "登入失敗次數過多,請稍後再試", | ||||||
|     "loginSuccessTitle": "Logged in", |     "loginSuccessTitle": "登入成功", | ||||||
|     "loginSuccessSubtitle": "Welcome back!", |     "loginSuccessSubtitle": "歡迎回來!", | ||||||
|     "loginOauthFailTitle": "An error occurred", |     "loginOauthFailTitle": "發生錯誤", | ||||||
|     "loginOauthFailSubtitle": "Failed to get OAuth URL", |     "loginOauthFailSubtitle": "無法取得 OAuth 網址", | ||||||
|     "loginOauthSuccessTitle": "Redirecting", |     "loginOauthSuccessTitle": "重新導向中", | ||||||
|     "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", |     "loginOauthSuccessSubtitle": "正在將您重新導向至 OAuth 供應商", | ||||||
|     "continueRedirectingTitle": "Redirecting...", |     "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", | ||||||
|     "continueRedirectingSubtitle": "You should be redirected to the app soon", |     "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", | ||||||
|     "continueInvalidRedirectTitle": "Invalid redirect", |     "loginOauthAutoRedirectButton": "Redirect now", | ||||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", |     "continueTitle": "繼續", | ||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueRedirectingTitle": "重新導向中……", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueRedirectingSubtitle": "您即將被重新導向至應用程式", | ||||||
|     "continueTitle": "Continue", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueSubtitle": "Click the button to continue to your app.", |     "continueInsecureRedirectTitle": "不安全的重新導向", | ||||||
|     "logoutFailTitle": "Failed to log out", |     "continueInsecureRedirectSubtitle": "您正嘗試從安全的 <code>https</code> 重新導向至不安全的 <code>http</code>。您確定要繼續嗎?", | ||||||
|     "logoutFailSubtitle": "Please try again", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "logoutSuccessTitle": "Logged out", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutSuccessSubtitle": "You have been logged out", |     "logoutFailTitle": "登出失敗", | ||||||
|     "logoutTitle": "Logout", |     "logoutFailSubtitle": "請再試一次", | ||||||
|     "logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.", |     "logoutSuccessTitle": "登出成功", | ||||||
|     "logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.", |     "logoutSuccessSubtitle": "您已成功登出", | ||||||
|     "notFoundTitle": "Page not found", |     "logoutTitle": "登出", | ||||||
|     "notFoundSubtitle": "The page you are looking for does not exist.", |     "logoutUsernameSubtitle": "您目前以 <code>{{username}}</code> 的身分登入。點擊下方按鈕以登出。", | ||||||
|     "notFoundButton": "Go home", |     "logoutOauthSubtitle": "您目前使用 {{provider}} OAuth 供應商並以 <code>{{username}}</code> 的身分登入。點擊下方按鈕以登出。", | ||||||
|     "totpFailTitle": "Failed to verify code", |     "notFoundTitle": "找不到頁面", | ||||||
|     "totpFailSubtitle": "Please check your code and try again", |     "notFoundSubtitle": "您要尋找的頁面不存在。", | ||||||
|     "totpSuccessTitle": "Verified", |     "notFoundButton": "回到首頁", | ||||||
|     "totpSuccessSubtitle": "Redirecting to your app", |     "totpFailTitle": "驗證失敗", | ||||||
|     "totpTitle": "Enter your TOTP code", |     "totpFailSubtitle": "請檢查您的驗證碼並再試一次", | ||||||
|     "totpSubtitle": "Please enter the code from your authenticator app.", |     "totpSuccessTitle": "驗證成功", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "totpSuccessSubtitle": "正在重新導向至您的應用程式", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "totpTitle": "輸入您的 TOTP 驗證碼", | ||||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", |     "totpSubtitle": "請輸入您驗證器應用程式中的代碼。", | ||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedTitle": "未經授權", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedResourceSubtitle": "使用者 <code>{{username}}</code> 未被授權存取資源 <code>{{resource}}</code>。", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "unauthorizedLoginSubtitle": "使用者 <code>{{username}}</code> 未被授權登入。", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |     "unauthorizedGroupsSubtitle": "使用者 <code>{{username}}</code> 不在存取資源 <code>{{resource}}</code> 所需的群組中。", | ||||||
|     "cancelTitle": "Cancel", |     "unauthorizedIpSubtitle": "您的 IP 位址 <code>{{ip}}</code> 未被授權存取資源 <code>{{resource}}</code>。", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "unauthorizedButton": "再試一次", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "cancelTitle": "取消", | ||||||
|     "errorTitle": "An error occurred", |     "forgotPasswordTitle": "忘記密碼?", | ||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information." |     "failedToFetchProvidersTitle": "載入驗證供應商失敗。請檢查您的設定。", | ||||||
|  |     "errorTitle": "發生錯誤", | ||||||
|  |     "errorSubtitle": "執行此操作時發生錯誤。請檢查主控台以獲取更多資訊。", | ||||||
|  |     "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", | ||||||
|  |     "fieldRequired": "This field is required", | ||||||
|  |     "invalidInput": "Invalid input", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -17,7 +17,7 @@ export const ForgotPasswordPage = () => { | |||||||
|       <CardHeader> |       <CardHeader> | ||||||
|         <CardTitle className="text-3xl">{t("forgotPasswordTitle")}</CardTitle> |         <CardTitle className="text-3xl">{t("forgotPasswordTitle")}</CardTitle> | ||||||
|         <CardDescription> |         <CardDescription> | ||||||
|           <Markdown>{forgotPasswordMessage}</Markdown> |           <Markdown>{forgotPasswordMessage !== "" ? forgotPasswordMessage : t('forgotPasswordMessage')}</Markdown> | ||||||
|         </CardDescription> |         </CardDescription> | ||||||
|       </CardHeader> |       </CardHeader> | ||||||
|     </Card> |     </Card> | ||||||
|   | |||||||
| @@ -162,9 +162,9 @@ export const LoginPage = () => { | |||||||
|           /> |           /> | ||||||
|         )} |         )} | ||||||
|         {configuredProviders.length == 0 && ( |         {configuredProviders.length == 0 && ( | ||||||
|           <h3 className="text-center text-xl text-red-600"> |           <p className="text-center text-red-600 max-w-sm"> | ||||||
|             {t("failedToFetchProvidersTitle")} |             {t("failedToFetchProvidersTitle")} | ||||||
|           </h3> |           </p> | ||||||
|         )} |         )} | ||||||
|       </CardContent> |       </CardContent> | ||||||
|     </Card> |     </Card> | ||||||
|   | |||||||
| @@ -17,8 +17,9 @@ export const UnauthorizedPage = () => { | |||||||
|   const username = searchParams.get("username"); |   const username = searchParams.get("username"); | ||||||
|   const resource = searchParams.get("resource"); |   const resource = searchParams.get("resource"); | ||||||
|   const groupErr = searchParams.get("groupErr"); |   const groupErr = searchParams.get("groupErr"); | ||||||
|  |   const ip = searchParams.get("ip"); | ||||||
|  |  | ||||||
|   if (!username) { |   if (!username && !ip) { | ||||||
|     return <Navigate to="/" />; |     return <Navigate to="/" />; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -41,6 +42,10 @@ export const UnauthorizedPage = () => { | |||||||
|     i18nKey = "unauthorizedGroupsSubtitle"; |     i18nKey = "unauthorizedGroupsSubtitle"; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   if (ip) { | ||||||
|  |     i18nKey = "unauthorizedIpSubtitle"; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Card className="min-w-xs sm:min-w-sm"> |     <Card className="min-w-xs sm:min-w-sm"> | ||||||
|       <CardHeader> |       <CardHeader> | ||||||
| @@ -55,6 +60,7 @@ export const UnauthorizedPage = () => { | |||||||
|             values={{ |             values={{ | ||||||
|               username, |               username, | ||||||
|               resource, |               resource, | ||||||
|  |               ip, | ||||||
|             }} |             }} | ||||||
|           /> |           /> | ||||||
|         </CardDescription> |         </CardDescription> | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								go.mod
									
									
									
									
									
								
							| @@ -4,23 +4,27 @@ go 1.23.2 | |||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/gin-gonic/gin v1.10.1 | 	github.com/gin-gonic/gin v1.10.1 | ||||||
| 	github.com/go-playground/validator/v10 v10.26.0 | 	github.com/go-playground/validator/v10 v10.27.0 | ||||||
| 	github.com/google/go-querystring v1.1.0 | 	github.com/google/go-querystring v1.1.0 | ||||||
| 	github.com/google/uuid v1.6.0 | 	github.com/google/uuid v1.6.0 | ||||||
| 	github.com/mdp/qrterminal/v3 v3.2.1 | 	github.com/mdp/qrterminal/v3 v3.2.1 | ||||||
| 	github.com/rs/zerolog v1.34.0 | 	github.com/rs/zerolog v1.34.0 | ||||||
| 	github.com/spf13/cobra v1.9.1 | 	github.com/spf13/cobra v1.9.1 | ||||||
| 	github.com/spf13/viper v1.20.1 | 	github.com/spf13/viper v1.20.1 | ||||||
| 	golang.org/x/crypto v0.39.0 | 	github.com/traefik/paerser v0.2.2 | ||||||
|  | 	golang.org/x/crypto v0.40.0 | ||||||
| ) | ) | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
|  | 	github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect | ||||||
|  | 	github.com/cenkalti/backoff/v5 v5.0.2 // indirect | ||||||
| 	github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect | 	github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect | ||||||
| 	github.com/charmbracelet/x/cellbuf v0.0.13 // indirect | 	github.com/charmbracelet/x/cellbuf v0.0.13 // indirect | ||||||
| 	github.com/containerd/errdefs v1.0.0 // indirect | 	github.com/containerd/errdefs v1.0.0 // indirect | ||||||
| 	github.com/containerd/errdefs/pkg v0.3.0 // indirect | 	github.com/containerd/errdefs/pkg v0.3.0 // indirect | ||||||
| 	github.com/containerd/log v0.1.0 // indirect | 	github.com/containerd/log v0.1.0 // indirect | ||||||
| 	github.com/go-viper/mapstructure/v2 v2.2.1 // 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/moby/sys/atomicwriter v0.1.0 // indirect | 	github.com/moby/sys/atomicwriter v0.1.0 // indirect | ||||||
| 	github.com/moby/term v0.5.2 // indirect | 	github.com/moby/term v0.5.2 // indirect | ||||||
| 	github.com/morikuni/aec v1.0.0 // indirect | 	github.com/morikuni/aec v1.0.0 // indirect | ||||||
| @@ -28,7 +32,7 @@ require ( | |||||||
| 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect | 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect | ||||||
| 	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect | 	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect | ||||||
| 	go.opentelemetry.io/otel/sdk v1.34.0 // indirect | 	go.opentelemetry.io/otel/sdk v1.34.0 // indirect | ||||||
| 	golang.org/x/term v0.32.0 // indirect | 	golang.org/x/term v0.33.0 // indirect | ||||||
| 	gotest.tools/v3 v3.5.2 // indirect | 	gotest.tools/v3 v3.5.2 // indirect | ||||||
| 	rsc.io/qr v0.2.0 // indirect | 	rsc.io/qr v0.2.0 // indirect | ||||||
| ) | ) | ||||||
| @@ -50,7 +54,7 @@ require ( | |||||||
| 	github.com/charmbracelet/x/term v0.2.1 // indirect | 	github.com/charmbracelet/x/term v0.2.1 // indirect | ||||||
| 	github.com/cloudwego/base64x v0.1.4 // indirect | 	github.com/cloudwego/base64x v0.1.4 // indirect | ||||||
| 	github.com/distribution/reference v0.6.0 // indirect | 	github.com/distribution/reference v0.6.0 // indirect | ||||||
| 	github.com/docker/docker v28.2.2+incompatible | 	github.com/docker/docker v28.3.2+incompatible | ||||||
| 	github.com/docker/go-connections v0.5.0 // indirect | 	github.com/docker/go-connections v0.5.0 // indirect | ||||||
| 	github.com/docker/go-units v0.5.0 // indirect | 	github.com/docker/go-units v0.5.0 // indirect | ||||||
| 	github.com/dustin/go-humanize v1.0.1 // indirect | 	github.com/dustin/go-humanize v1.0.1 // indirect | ||||||
| @@ -59,6 +63,7 @@ require ( | |||||||
| 	github.com/fsnotify/fsnotify v1.8.0 // indirect | 	github.com/fsnotify/fsnotify v1.8.0 // indirect | ||||||
| 	github.com/gabriel-vasile/mimetype v1.4.8 // indirect | 	github.com/gabriel-vasile/mimetype v1.4.8 // indirect | ||||||
| 	github.com/gin-contrib/sse v1.0.0 // indirect | 	github.com/gin-contrib/sse v1.0.0 // indirect | ||||||
|  | 	github.com/go-ldap/ldap/v3 v3.4.11 | ||||||
| 	github.com/go-logr/logr v1.4.2 // indirect | 	github.com/go-logr/logr v1.4.2 // indirect | ||||||
| 	github.com/go-logr/stdr v1.2.2 // indirect | 	github.com/go-logr/stdr v1.2.2 // indirect | ||||||
| 	github.com/go-playground/locales v0.14.1 // indirect | 	github.com/go-playground/locales v0.14.1 // indirect | ||||||
| @@ -105,11 +110,11 @@ require ( | |||||||
| 	go.uber.org/atomic v1.9.0 // indirect | 	go.uber.org/atomic v1.9.0 // indirect | ||||||
| 	go.uber.org/multierr v1.9.0 // indirect | 	go.uber.org/multierr v1.9.0 // indirect | ||||||
| 	golang.org/x/arch v0.13.0 // indirect | 	golang.org/x/arch v0.13.0 // indirect | ||||||
| 	golang.org/x/net v0.38.0 // indirect | 	golang.org/x/net v0.41.0 // indirect | ||||||
| 	golang.org/x/oauth2 v0.30.0 | 	golang.org/x/oauth2 v0.30.0 | ||||||
| 	golang.org/x/sync v0.15.0 // indirect | 	golang.org/x/sync v0.16.0 // indirect | ||||||
| 	golang.org/x/sys v0.33.0 // indirect | 	golang.org/x/sys v0.34.0 // indirect | ||||||
| 	golang.org/x/text v0.26.0 // indirect | 	golang.org/x/text v0.27.0 // indirect | ||||||
| 	google.golang.org/protobuf v1.36.3 // indirect | 	google.golang.org/protobuf v1.36.3 // indirect | ||||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||||
| ) | ) | ||||||
|   | |||||||
							
								
								
									
										62
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										62
									
								
								go.sum
									
									
									
									
									
								
							| @@ -1,9 +1,13 @@ | |||||||
| github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= | ||||||
| github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= | ||||||
|  | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= | ||||||
|  | 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 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= | ||||||
| github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= | github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= | ||||||
| github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= | github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= | ||||||
| github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= | github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= | ||||||
|  | github.com/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= | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= | ||||||
| github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= | ||||||
| github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= | ||||||
| @@ -22,6 +26,8 @@ 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/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= | ||||||
| github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= | ||||||
| github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= | ||||||
|  | github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= | ||||||
|  | github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= | ||||||
| github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= | 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/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 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= | ||||||
| @@ -68,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/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 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= | ||||||
| github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= | ||||||
| github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= | github.com/docker/docker v28.3.2+incompatible h1:wn66NJ6pWB1vBZIilP8G3qQPqHy5XymfYn5vsqeA5oA= | ||||||
| github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= | github.com/docker/docker v28.3.2+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 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= | ||||||
| github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= | ||||||
| github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= | ||||||
| @@ -90,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-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 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= | ||||||
| github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= | github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= | ||||||
|  | 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-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= | ||||||
| github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= | 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.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= | ||||||
| @@ -101,10 +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/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= | ||||||
| github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= | ||||||
| github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= | ||||||
| github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= | github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= | ||||||
| github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= | github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= | ||||||
| github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= | github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= | ||||||
| github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= | github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= | ||||||
| github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= | github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= | ||||||
| github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= | github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= | ||||||
| github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= | ||||||
| @@ -126,8 +136,22 @@ 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/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 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= | ||||||
| github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= | github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= | ||||||
|  | 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= | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= | ||||||
| github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= | ||||||
|  | github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= | ||||||
|  | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= | ||||||
|  | github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= | ||||||
|  | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= | ||||||
|  | github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= | ||||||
|  | github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= | ||||||
|  | github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= | ||||||
|  | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= | ||||||
|  | github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= | ||||||
|  | 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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= | ||||||
| github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= | ||||||
| github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= | ||||||
| @@ -238,6 +262,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf | |||||||
| github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | ||||||
| github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= | ||||||
| github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= | ||||||
|  | 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 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= | ||||||
| github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= | ||||||
| github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= | ||||||
| @@ -273,8 +299,8 @@ 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-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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||||
| golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||||
| golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= | golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= | ||||||
| golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= | golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= | ||||||
| golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= | 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-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= | ||||||
| golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||||
| @@ -283,15 +309,15 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn | |||||||
| golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | 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-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.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= | ||||||
| golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= | golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= | ||||||
| golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= | golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= | ||||||
| golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= | 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/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-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-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.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
| golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= | golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= | ||||||
| golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= | 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-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-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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
| @@ -301,14 +327,14 @@ 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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= | golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= | ||||||
| golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= | golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= | ||||||
| golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= | golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= | ||||||
| golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= | golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= | ||||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||||
| golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= | golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= | ||||||
| golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= | golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= | ||||||
| golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= | 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/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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||||
|   | |||||||
| @@ -1,137 +0,0 @@ | |||||||
| package api |  | ||||||
|  |  | ||||||
| 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" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func NewAPI(config types.APIConfig, handlers *handlers.Handlers) *API { |  | ||||||
| 	return &API{ |  | ||||||
| 		Config:   config, |  | ||||||
| 		Handlers: handlers, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type API struct { |  | ||||||
| 	Config   types.APIConfig |  | ||||||
| 	Router   *gin.Engine |  | ||||||
| 	Handlers *handlers.Handlers |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (api *API) Init() { |  | ||||||
| 	// Disable gin logs |  | ||||||
| 	gin.SetMode(gin.ReleaseMode) |  | ||||||
|  |  | ||||||
| 	// Create router and use zerolog for logs |  | ||||||
| 	log.Debug().Msg("Setting up router") |  | ||||||
| 	router := gin.New() |  | ||||||
| 	router.Use(zerolog()) |  | ||||||
|  |  | ||||||
| 	// Read UI assets |  | ||||||
| 	log.Debug().Msg("Setting up assets") |  | ||||||
| 	dist, err := fs.Sub(assets.Assets, "dist") |  | ||||||
|  |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Fatal().Err(err).Msg("Failed to get UI assets") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Create file server |  | ||||||
| 	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") { |  | ||||||
| 			// Check if the file exists |  | ||||||
| 			_, err := fs.Stat(dist, strings.TrimPrefix(c.Request.URL.Path, "/")) |  | ||||||
|  |  | ||||||
| 			// If the file doesn't exist, serve the index.html |  | ||||||
| 			if os.IsNotExist(err) { |  | ||||||
| 				c.Request.URL.Path = "/" |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			// Serve the file |  | ||||||
| 			fileServer.ServeHTTP(c.Writer, c.Request) |  | ||||||
|  |  | ||||||
| 			// Stop further processing |  | ||||||
| 			c.Abort() |  | ||||||
| 		} |  | ||||||
| 	}) |  | ||||||
|  |  | ||||||
| 	// Set router |  | ||||||
| 	api.Router = router |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (api *API) SetupRoutes() { |  | ||||||
| 	// Proxy |  | ||||||
| 	api.Router.GET("/api/auth/:proxy", api.Handlers.AuthHandler) |  | ||||||
|  |  | ||||||
| 	// Auth |  | ||||||
| 	api.Router.POST("/api/login", api.Handlers.LoginHandler) |  | ||||||
| 	api.Router.POST("/api/totp", api.Handlers.TotpHandler) |  | ||||||
| 	api.Router.POST("/api/logout", api.Handlers.LogoutHandler) |  | ||||||
|  |  | ||||||
| 	// Context |  | ||||||
| 	api.Router.GET("/api/app", api.Handlers.AppHandler) |  | ||||||
| 	api.Router.GET("/api/user", api.Handlers.UserHandler) |  | ||||||
|  |  | ||||||
| 	// OAuth |  | ||||||
| 	api.Router.GET("/api/oauth/url/:provider", api.Handlers.OauthUrlHandler) |  | ||||||
| 	api.Router.GET("/api/oauth/callback/:provider", api.Handlers.OauthCallbackHandler) |  | ||||||
|  |  | ||||||
| 	// App |  | ||||||
| 	api.Router.GET("/api/healthcheck", api.Handlers.HealthcheckHandler) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (api *API) Run() { |  | ||||||
| 	log.Info().Str("address", api.Config.Address).Int("port", api.Config.Port).Msg("Starting server") |  | ||||||
|  |  | ||||||
| 	// Run server |  | ||||||
| 	err := api.Router.Run(fmt.Sprintf("%s:%d", api.Config.Address, api.Config.Port)) |  | ||||||
|  |  | ||||||
| 	// Check for errors |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Fatal().Err(err).Msg("Failed to start server") |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // zerolog is a middleware for gin that logs requests using zerolog |  | ||||||
| func zerolog() gin.HandlerFunc { |  | ||||||
| 	return func(c *gin.Context) { |  | ||||||
| 		// Get initial time |  | ||||||
| 		tStart := time.Now() |  | ||||||
|  |  | ||||||
| 		// Process request |  | ||||||
| 		c.Next() |  | ||||||
|  |  | ||||||
| 		// Get status code, address, method and path |  | ||||||
| 		code := c.Writer.Status() |  | ||||||
| 		address := c.Request.RemoteAddr |  | ||||||
| 		method := c.Request.Method |  | ||||||
| 		path := c.Request.URL.Path |  | ||||||
|  |  | ||||||
| 		// Get latency |  | ||||||
| 		latency := time.Since(tStart).String() |  | ||||||
|  |  | ||||||
| 		// Log request |  | ||||||
| 		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") |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -7,6 +7,7 @@ import ( | |||||||
| 	"sync" | 	"sync" | ||||||
| 	"time" | 	"time" | ||||||
| 	"tinyauth/internal/docker" | 	"tinyauth/internal/docker" | ||||||
|  | 	"tinyauth/internal/ldap" | ||||||
| 	"tinyauth/internal/types" | 	"tinyauth/internal/types" | ||||||
| 	"tinyauth/internal/utils" | 	"tinyauth/internal/utils" | ||||||
|  |  | ||||||
| @@ -16,60 +17,139 @@ import ( | |||||||
| 	"golang.org/x/crypto/bcrypt" | 	"golang.org/x/crypto/bcrypt" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func NewAuth(config types.AuthConfig, docker *docker.Docker) *Auth { |  | ||||||
| 	return &Auth{ |  | ||||||
| 		Config:        config, |  | ||||||
| 		Docker:        docker, |  | ||||||
| 		LoginAttempts: make(map[string]*types.LoginAttempt), |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type Auth struct { | type Auth struct { | ||||||
| 	Config        types.AuthConfig | 	Config        types.AuthConfig | ||||||
| 	Docker        *docker.Docker | 	Docker        *docker.Docker | ||||||
| 	LoginAttempts map[string]*types.LoginAttempt | 	LoginAttempts map[string]*types.LoginAttempt | ||||||
| 	LoginMutex    sync.RWMutex | 	LoginMutex    sync.RWMutex | ||||||
|  | 	Store         *sessions.CookieStore | ||||||
|  | 	LDAP          *ldap.LDAP | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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{ | ||||||
|  | 		Config:        config, | ||||||
|  | 		Docker:        docker, | ||||||
|  | 		LoginAttempts: make(map[string]*types.LoginAttempt), | ||||||
|  | 		Store:         store, | ||||||
|  | 		LDAP:          ldap, | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *Auth) GetSession(c *gin.Context) (*sessions.Session, error) { | func (auth *Auth) GetSession(c *gin.Context) (*sessions.Session, error) { | ||||||
| 	// Create cookie store | 	session, err := auth.Store.Get(c.Request, auth.Config.SessionCookieName) | ||||||
| 	store := sessions.NewCookieStore([]byte(auth.Config.Secret)) |  | ||||||
|  |  | ||||||
| 	// Configure cookie store | 	// If there was an error getting the session, it might be invalid so let's clear it and retry | ||||||
| 	store.Options = &sessions.Options{ |  | ||||||
| 		Path:     "/", |  | ||||||
| 		MaxAge:   auth.Config.SessionExpiry, |  | ||||||
| 		Secure:   auth.Config.CookieSecure, |  | ||||||
| 		HttpOnly: true, |  | ||||||
| 		Domain:   fmt.Sprintf(".%s", auth.Config.Domain), |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Get session |  | ||||||
| 	session, err := store.Get(c.Request, auth.Config.SessionCookieName) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error().Err(err).Msg("Failed to get session") | 		log.Error().Err(err).Msg("Invalid session, clearing cookie and retrying") | ||||||
| 		return nil, err | 		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) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Error().Err(err).Msg("Failed to get session") | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return session, nil | 	return session, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *Auth) GetUser(username string) *types.User { | func (auth *Auth) SearchUser(username string) types.UserSearch { | ||||||
| 	// Loop through users and return the user if the username matches | 	log.Debug().Str("username", username).Msg("Searching for user") | ||||||
| 	for _, user := range auth.Config.Users { |  | ||||||
| 		if user.Username == username { | 	// Check local users first | ||||||
| 			return &user | 	if auth.GetLocalUser(username).Username != "" { | ||||||
|  | 		log.Debug().Str("username", username).Msg("Found local user") | ||||||
|  | 		return types.UserSearch{ | ||||||
|  | 			Username: username, | ||||||
|  | 			Type:     "local", | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return nil |  | ||||||
|  | 	// 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{} | ||||||
|  | 		} | ||||||
|  | 		return types.UserSearch{ | ||||||
|  | 			Username: userDN, | ||||||
|  | 			Type:     "ldap", | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return types.UserSearch{ | ||||||
|  | 		Type: "unknown", | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (auth *Auth) VerifyUser(search types.UserSearch, password string) bool { | ||||||
|  | 	// Authenticate the user based on the type | ||||||
|  | 	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") | ||||||
|  | 		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") | ||||||
|  |  | ||||||
|  | 	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{} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *Auth) CheckPassword(user types.User, password string) bool { | func (auth *Auth) CheckPassword(user types.User, password string) bool { | ||||||
| 	// Compare the hashed password with the password provided |  | ||||||
| 	return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil | 	return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // IsAccountLocked checks if a username or IP is locked due to too many failed login attempts |  | ||||||
| func (auth *Auth) IsAccountLocked(identifier string) (bool, int) { | func (auth *Auth) IsAccountLocked(identifier string) (bool, int) { | ||||||
| 	auth.LoginMutex.RLock() | 	auth.LoginMutex.RLock() | ||||||
| 	defer auth.LoginMutex.RUnlock() | 	defer auth.LoginMutex.RUnlock() | ||||||
| @@ -96,7 +176,6 @@ func (auth *Auth) IsAccountLocked(identifier string) (bool, int) { | |||||||
| 	return false, 0 | 	return false, 0 | ||||||
| } | } | ||||||
|  |  | ||||||
| // RecordLoginAttempt records a login attempt for rate limiting |  | ||||||
| func (auth *Auth) RecordLoginAttempt(identifier string, success bool) { | func (auth *Auth) RecordLoginAttempt(identifier string, success bool) { | ||||||
| 	// Skip if rate limiting is not configured | 	// Skip if rate limiting is not configured | ||||||
| 	if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 { | 	if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 { | ||||||
| @@ -133,14 +212,13 @@ func (auth *Auth) RecordLoginAttempt(identifier string, success bool) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *Auth) EmailWhitelisted(emailSrc string) bool { | func (auth *Auth) EmailWhitelisted(email string) bool { | ||||||
| 	return utils.CheckWhitelist(auth.Config.OauthWhitelist, emailSrc) | 	return utils.CheckFilter(auth.Config.OauthWhitelist, email) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) error { | func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) error { | ||||||
| 	log.Debug().Msg("Creating session cookie") | 	log.Debug().Msg("Creating session cookie") | ||||||
|  |  | ||||||
| 	// Get session |  | ||||||
| 	session, err := auth.GetSession(c) | 	session, err := auth.GetSession(c) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error().Err(err).Msg("Failed to get session") | 		log.Error().Err(err).Msg("Failed to get session") | ||||||
| @@ -149,7 +227,6 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) | |||||||
|  |  | ||||||
| 	log.Debug().Msg("Setting session cookie") | 	log.Debug().Msg("Setting session cookie") | ||||||
|  |  | ||||||
| 	// Calculate expiry |  | ||||||
| 	var sessionExpiry int | 	var sessionExpiry int | ||||||
|  |  | ||||||
| 	if data.TotpPending { | 	if data.TotpPending { | ||||||
| @@ -158,7 +235,6 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) | |||||||
| 		sessionExpiry = auth.Config.SessionExpiry | 		sessionExpiry = auth.Config.SessionExpiry | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Set data |  | ||||||
| 	session.Values["username"] = data.Username | 	session.Values["username"] = data.Username | ||||||
| 	session.Values["name"] = data.Name | 	session.Values["name"] = data.Name | ||||||
| 	session.Values["email"] = data.Email | 	session.Values["email"] = data.Email | ||||||
| @@ -167,21 +243,18 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) | |||||||
| 	session.Values["totpPending"] = data.TotpPending | 	session.Values["totpPending"] = data.TotpPending | ||||||
| 	session.Values["oauthGroups"] = data.OAuthGroups | 	session.Values["oauthGroups"] = data.OAuthGroups | ||||||
|  |  | ||||||
| 	// Save session |  | ||||||
| 	err = session.Save(c.Request, c.Writer) | 	err = session.Save(c.Request, c.Writer) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error().Err(err).Msg("Failed to save session") | 		log.Error().Err(err).Msg("Failed to save session") | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Return nil |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *Auth) DeleteSessionCookie(c *gin.Context) error { | func (auth *Auth) DeleteSessionCookie(c *gin.Context) error { | ||||||
| 	log.Debug().Msg("Deleting session cookie") | 	log.Debug().Msg("Deleting session cookie") | ||||||
|  |  | ||||||
| 	// Get session |  | ||||||
| 	session, err := auth.GetSession(c) | 	session, err := auth.GetSession(c) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error().Err(err).Msg("Failed to get session") | 		log.Error().Err(err).Msg("Failed to get session") | ||||||
| @@ -193,21 +266,18 @@ func (auth *Auth) DeleteSessionCookie(c *gin.Context) error { | |||||||
| 		delete(session.Values, key) | 		delete(session.Values, key) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Save session |  | ||||||
| 	err = session.Save(c.Request, c.Writer) | 	err = session.Save(c.Request, c.Writer) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error().Err(err).Msg("Failed to save session") | 		log.Error().Err(err).Msg("Failed to save session") | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Return nil |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) { | func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) { | ||||||
| 	log.Debug().Msg("Getting session cookie") | 	log.Debug().Msg("Getting session cookie") | ||||||
|  |  | ||||||
| 	// Get session |  | ||||||
| 	session, err := auth.GetSession(c) | 	session, err := auth.GetSession(c) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error().Err(err).Msg("Failed to get session") | 		log.Error().Err(err).Msg("Failed to get session") | ||||||
| @@ -216,7 +286,6 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) | |||||||
|  |  | ||||||
| 	log.Debug().Msg("Got session") | 	log.Debug().Msg("Got session") | ||||||
|  |  | ||||||
| 	// Get data from session |  | ||||||
| 	username, usernameOk := session.Values["username"].(string) | 	username, usernameOk := session.Values["username"].(string) | ||||||
| 	email, emailOk := session.Values["email"].(string) | 	email, emailOk := session.Values["email"].(string) | ||||||
| 	name, nameOk := session.Values["name"].(string) | 	name, nameOk := session.Values["name"].(string) | ||||||
| @@ -225,30 +294,21 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) | |||||||
| 	totpPending, totpPendingOk := session.Values["totpPending"].(bool) | 	totpPending, totpPendingOk := session.Values["totpPending"].(bool) | ||||||
| 	oauthGroups, oauthGroupsOk := session.Values["oauthGroups"].(string) | 	oauthGroups, oauthGroupsOk := session.Values["oauthGroups"].(string) | ||||||
|  |  | ||||||
|  | 	// If any data is missing, delete the session cookie | ||||||
| 	if !usernameOk || !providerOK || !expiryOk || !totpPendingOk || !emailOk || !nameOk || !oauthGroupsOk { | 	if !usernameOk || !providerOK || !expiryOk || !totpPendingOk || !emailOk || !nameOk || !oauthGroupsOk { | ||||||
| 		log.Warn().Msg("Session cookie is invalid") | 		log.Warn().Msg("Session cookie is invalid") | ||||||
|  |  | ||||||
| 		// If any data is missing, delete the session cookie |  | ||||||
| 		auth.DeleteSessionCookie(c) | 		auth.DeleteSessionCookie(c) | ||||||
|  |  | ||||||
| 		// Return empty cookie |  | ||||||
| 		return types.SessionCookie{}, nil | 		return types.SessionCookie{}, nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Check if the cookie has expired | 	// If the session cookie has expired, delete it | ||||||
| 	if time.Now().Unix() > expiry { | 	if time.Now().Unix() > expiry { | ||||||
| 		log.Warn().Msg("Session cookie expired") | 		log.Warn().Msg("Session cookie expired") | ||||||
|  |  | ||||||
| 		// If it has, delete it |  | ||||||
| 		auth.DeleteSessionCookie(c) | 		auth.DeleteSessionCookie(c) | ||||||
|  |  | ||||||
| 		// Return empty cookie |  | ||||||
| 		return types.SessionCookie{}, nil | 		return types.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") | 	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 the cookie |  | ||||||
| 	return types.SessionCookie{ | 	return types.SessionCookie{ | ||||||
| 		Username:    username, | 		Username:    username, | ||||||
| 		Name:        name, | 		Name:        name, | ||||||
| @@ -260,26 +320,22 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) | |||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *Auth) UserAuthConfigured() bool { | func (auth *Auth) UserAuthConfigured() bool { | ||||||
| 	// If there are users, return true | 	// If there are users or LDAP is configured, return true | ||||||
| 	return len(auth.Config.Users) > 0 | 	return len(auth.Config.Users) > 0 || auth.LDAP != nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext, labels types.TinyauthLabels) bool { | func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext, labels types.Labels) bool { | ||||||
| 	// Check if oauth is allowed |  | ||||||
| 	if context.OAuth { | 	if context.OAuth { | ||||||
| 		log.Debug().Msg("Checking OAuth whitelist") | 		log.Debug().Msg("Checking OAuth whitelist") | ||||||
| 		return utils.CheckWhitelist(labels.OAuthWhitelist, context.Email) | 		return utils.CheckFilter(labels.OAuth.Whitelist, context.Email) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Check users |  | ||||||
| 	log.Debug().Msg("Checking users") | 	log.Debug().Msg("Checking users") | ||||||
|  | 	return utils.CheckFilter(labels.Users, context.Username) | ||||||
| 	return utils.CheckWhitelist(labels.Users, context.Username) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *Auth) OAuthGroup(c *gin.Context, context types.UserContext, labels types.TinyauthLabels) bool { | func (auth *Auth) OAuthGroup(c *gin.Context, context types.UserContext, labels types.Labels) bool { | ||||||
| 	// Check if groups are required | 	if labels.OAuth.Groups == "" { | ||||||
| 	if labels.OAuthGroups == "" { |  | ||||||
| 		return true | 		return true | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -294,7 +350,7 @@ func (auth *Auth) OAuthGroup(c *gin.Context, context types.UserContext, labels t | |||||||
|  |  | ||||||
| 	// For every group check if it is in the required groups | 	// For every group check if it is in the required groups | ||||||
| 	for _, group := range oauthGroups { | 	for _, group := range oauthGroups { | ||||||
| 		if utils.CheckWhitelist(labels.OAuthGroups, group) { | 		if utils.CheckFilter(labels.OAuth.Groups, group) { | ||||||
| 			log.Debug().Str("group", group).Msg("Group is in required groups") | 			log.Debug().Str("group", group).Msg("Group is in required groups") | ||||||
| 			return true | 			return true | ||||||
| 		} | 		} | ||||||
| @@ -302,18 +358,12 @@ func (auth *Auth) OAuthGroup(c *gin.Context, context types.UserContext, labels t | |||||||
|  |  | ||||||
| 	// No groups matched | 	// No groups matched | ||||||
| 	log.Debug().Msg("No groups matched") | 	log.Debug().Msg("No groups matched") | ||||||
|  |  | ||||||
| 	// Return false |  | ||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *Auth) AuthEnabled(c *gin.Context, labels types.TinyauthLabels) (bool, error) { | func (auth *Auth) AuthEnabled(uri string, labels types.Labels) (bool, error) { | ||||||
| 	// Get headers | 	// If the label is empty, auth is enabled | ||||||
| 	uri := c.Request.Header.Get("X-Forwarded-Uri") |  | ||||||
|  |  | ||||||
| 	// Check if the allowed label is empty |  | ||||||
| 	if labels.Allowed == "" { | 	if labels.Allowed == "" { | ||||||
| 		// Auth enabled |  | ||||||
| 		return true, nil | 		return true, nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -322,13 +372,12 @@ func (auth *Auth) AuthEnabled(c *gin.Context, labels types.TinyauthLabels) (bool | |||||||
|  |  | ||||||
| 	// If there is an error, invalid regex, auth enabled | 	// If there is an error, invalid regex, auth enabled | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Warn().Err(err).Msg("Invalid regex") | 		log.Error().Err(err).Msg("Invalid regex") | ||||||
| 		return true, err | 		return true, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Check if the uri matches the regex | 	// If the regex matches the URI, auth is not enabled | ||||||
| 	if regex.MatchString(uri) { | 	if regex.MatchString(uri) { | ||||||
| 		// Auth disabled |  | ||||||
| 		return false, nil | 		return false, nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -337,17 +386,67 @@ func (auth *Auth) AuthEnabled(c *gin.Context, labels types.TinyauthLabels) (bool | |||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *Auth) GetBasicAuth(c *gin.Context) *types.User { | func (auth *Auth) GetBasicAuth(c *gin.Context) *types.User { | ||||||
| 	// Get the Authorization header |  | ||||||
| 	username, password, ok := c.Request.BasicAuth() | 	username, password, ok := c.Request.BasicAuth() | ||||||
|  |  | ||||||
| 	// If not ok, return an empty user |  | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Return the user |  | ||||||
| 	return &types.User{ | 	return &types.User{ | ||||||
| 		Username: username, | 		Username: username, | ||||||
| 		Password: password, | 		Password: password, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (auth *Auth) CheckIP(labels types.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") | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if res { | ||||||
|  | 			log.Warn().Str("ip", ip).Str("item", blocked).Msg("IP is in blocked list, denying access") | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 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 { | ||||||
|  | 			log.Error().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list") | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if res { | ||||||
|  | 			log.Debug().Str("ip", ip).Str("item", allowed).Msg("IP is in allowed list, allowing access") | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 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") | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log.Debug().Str("ip", ip).Msg("IP not in allow or block list, allowing by default") | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (auth *Auth) BypassedIP(labels types.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") | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if res { | ||||||
|  | 			log.Debug().Str("ip", ip).Str("item", bypassed).Msg("IP is in bypass list, allowing access") | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log.Debug().Str("ip", ip).Msg("IP not in bypass list, continuing with authentication") | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ import ( | |||||||
| 	"testing" | 	"testing" | ||||||
| 	"time" | 	"time" | ||||||
| 	"tinyauth/internal/auth" | 	"tinyauth/internal/auth" | ||||||
| 	"tinyauth/internal/docker" |  | ||||||
| 	"tinyauth/internal/types" | 	"tinyauth/internal/types" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -18,7 +17,7 @@ func TestLoginRateLimiting(t *testing.T) { | |||||||
| 	// Initialize a new auth service with 3 max retries and 5 seconds timeout | 	// Initialize a new auth service with 3 max retries and 5 seconds timeout | ||||||
| 	config.LoginMaxRetries = 3 | 	config.LoginMaxRetries = 3 | ||||||
| 	config.LoginTimeout = 5 | 	config.LoginTimeout = 5 | ||||||
| 	authService := auth.NewAuth(config, &docker.Docker{}) | 	authService := auth.NewAuth(config, nil, nil) | ||||||
|  |  | ||||||
| 	// Test identifier | 	// Test identifier | ||||||
| 	identifier := "test_user" | 	identifier := "test_user" | ||||||
| @@ -62,7 +61,7 @@ func TestLoginRateLimiting(t *testing.T) { | |||||||
| 	// Reinitialize auth service with a shorter timeout for testing | 	// Reinitialize auth service with a shorter timeout for testing | ||||||
| 	config.LoginTimeout = 1 | 	config.LoginTimeout = 1 | ||||||
| 	config.LoginMaxRetries = 3 | 	config.LoginMaxRetries = 3 | ||||||
| 	authService = auth.NewAuth(config, &docker.Docker{}) | 	authService = auth.NewAuth(config, nil, nil) | ||||||
|  |  | ||||||
| 	// Add enough failed attempts to lock the account | 	// Add enough failed attempts to lock the account | ||||||
| 	for i := 0; i < 3; i++ { | 	for i := 0; i < 3; i++ { | ||||||
| @@ -87,7 +86,7 @@ func TestLoginRateLimiting(t *testing.T) { | |||||||
| 	t.Log("Testing disabled rate limiting") | 	t.Log("Testing disabled rate limiting") | ||||||
| 	config.LoginMaxRetries = 0 | 	config.LoginMaxRetries = 0 | ||||||
| 	config.LoginTimeout = 0 | 	config.LoginTimeout = 0 | ||||||
| 	authService = auth.NewAuth(config, &docker.Docker{}) | 	authService = auth.NewAuth(config, nil, nil) | ||||||
|  |  | ||||||
| 	for i := 0; i < 10; i++ { | 	for i := 0; i < 10; i++ { | ||||||
| 		authService.RecordLoginAttempt(identifier, false) | 		authService.RecordLoginAttempt(identifier, false) | ||||||
| @@ -103,7 +102,7 @@ func TestConcurrentLoginAttempts(t *testing.T) { | |||||||
| 	// Initialize a new auth service with 2 max retries and 5 seconds timeout | 	// Initialize a new auth service with 2 max retries and 5 seconds timeout | ||||||
| 	config.LoginMaxRetries = 2 | 	config.LoginMaxRetries = 2 | ||||||
| 	config.LoginTimeout = 5 | 	config.LoginTimeout = 5 | ||||||
| 	authService := auth.NewAuth(config, &docker.Docker{}) | 	authService := auth.NewAuth(config, nil, nil) | ||||||
|  |  | ||||||
| 	// Test multiple identifiers | 	// Test multiple identifiers | ||||||
| 	identifiers := []string{"user1", "user2", "user3"} | 	identifiers := []string{"user1", "user2", "user3"} | ||||||
|   | |||||||
| @@ -1,20 +1,11 @@ | |||||||
| package constants | package constants | ||||||
|  |  | ||||||
| // TinyauthLabels is a list of labels that can be used in a tinyauth protected container | // Claims are the OIDC supported claims (prefered username is included for convinience) | ||||||
| var TinyauthLabels = []string{ |  | ||||||
| 	"tinyauth.oauth.whitelist", |  | ||||||
| 	"tinyauth.users", |  | ||||||
| 	"tinyauth.allowed", |  | ||||||
| 	"tinyauth.headers", |  | ||||||
| 	"tinyauth.oauth.groups", |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Claims are the OIDC supported claims (including preferd username for some reason) |  | ||||||
| type Claims struct { | type Claims struct { | ||||||
| 	Name              string   `json:"name"` | 	Name              string `json:"name"` | ||||||
| 	Email             string   `json:"email"` | 	Email             string `json:"email"` | ||||||
| 	PreferredUsername string   `json:"preferred_username"` | 	PreferredUsername string `json:"preferred_username"` | ||||||
| 	Groups            []string `json:"groups"` | 	Groups            any    `json:"groups"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // Version information | // Version information | ||||||
| @@ -22,7 +13,7 @@ var Version = "development" | |||||||
| var CommitHash = "n/a" | var CommitHash = "n/a" | ||||||
| var BuildTimestamp = "n/a" | var BuildTimestamp = "n/a" | ||||||
|  |  | ||||||
| // Cookie names | // Base cookie names | ||||||
| var SessionCookieName = "tinyauth-session" | var SessionCookieName = "tinyauth-session" | ||||||
| var CsrfCookieName = "tinyauth-csrf" | var CsrfCookieName = "tinyauth-csrf" | ||||||
| var RedirectCookieName = "tinyauth-redirect" | var RedirectCookieName = "tinyauth-redirect" | ||||||
|   | |||||||
| @@ -11,119 +11,92 @@ import ( | |||||||
| 	"github.com/rs/zerolog/log" | 	"github.com/rs/zerolog/log" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func NewDocker() *Docker { |  | ||||||
| 	return &Docker{} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type Docker struct { | type Docker struct { | ||||||
| 	Client  *client.Client | 	Client  *client.Client | ||||||
| 	Context context.Context | 	Context context.Context | ||||||
| } | } | ||||||
|  |  | ||||||
| func (docker *Docker) Init() error { | func NewDocker() (*Docker, error) { | ||||||
| 	// Create a new docker client |  | ||||||
| 	client, err := client.NewClientWithOpts(client.FromEnv) | 	client, err := client.NewClientWithOpts(client.FromEnv) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Create the context |  | ||||||
| 	docker.Context = context.Background() |  | ||||||
|  |  | ||||||
| 	// Negotiate API version |  | ||||||
| 	client.NegotiateAPIVersion(docker.Context) |  | ||||||
|  |  | ||||||
| 	// Set client |  | ||||||
| 	docker.Client = client |  | ||||||
|  |  | ||||||
| 	// Done |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (docker *Docker) GetContainers() ([]container.Summary, error) { |  | ||||||
| 	// Get the list of containers |  | ||||||
| 	containers, err := docker.Client.ContainerList(docker.Context, container.ListOptions{}) |  | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Return the containers | 	ctx := context.Background() | ||||||
|  | 	client.NegotiateAPIVersion(ctx) | ||||||
|  |  | ||||||
|  | 	return &Docker{ | ||||||
|  | 		Client:  client, | ||||||
|  | 		Context: ctx, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (docker *Docker) GetContainers() ([]container.Summary, error) { | ||||||
|  | 	containers, err := docker.Client.ContainerList(docker.Context, container.ListOptions{}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
| 	return containers, nil | 	return containers, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (docker *Docker) InspectContainer(containerId string) (container.InspectResponse, error) { | func (docker *Docker) InspectContainer(containerId string) (container.InspectResponse, error) { | ||||||
| 	// Inspect the container |  | ||||||
| 	inspect, err := docker.Client.ContainerInspect(docker.Context, containerId) | 	inspect, err := docker.Client.ContainerInspect(docker.Context, containerId) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return container.InspectResponse{}, err | 		return container.InspectResponse{}, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Return the inspect |  | ||||||
| 	return inspect, nil | 	return inspect, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (docker *Docker) DockerConnected() bool { | func (docker *Docker) DockerConnected() bool { | ||||||
| 	// Ping the docker client if there is an error it is not connected |  | ||||||
| 	_, err := docker.Client.Ping(docker.Context) | 	_, err := docker.Client.Ping(docker.Context) | ||||||
| 	return err == nil | 	return err == nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (docker *Docker) GetLabels(appId string) (types.TinyauthLabels, error) { | func (docker *Docker) GetLabels(app string, domain string) (types.Labels, error) { | ||||||
| 	// Check if we have access to the Docker API |  | ||||||
| 	isConnected := docker.DockerConnected() | 	isConnected := docker.DockerConnected() | ||||||
|  |  | ||||||
| 	// If we don't have access, return an empty struct |  | ||||||
| 	if !isConnected { | 	if !isConnected { | ||||||
| 		log.Debug().Msg("Docker not connected, returning empty labels") | 		log.Debug().Msg("Docker not connected, returning empty labels") | ||||||
| 		return types.TinyauthLabels{}, nil | 		return types.Labels{}, nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Get the containers | 	log.Debug().Msg("Getting containers") | ||||||
|  |  | ||||||
| 	containers, err := docker.GetContainers() | 	containers, err := docker.GetContainers() | ||||||
|  |  | ||||||
| 	// If there is an error, return false |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return types.TinyauthLabels{}, err | 		log.Error().Err(err).Msg("Error getting containers") | ||||||
|  | 		return types.Labels{}, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Got containers") |  | ||||||
|  |  | ||||||
| 	// Loop through the containers |  | ||||||
| 	for _, container := range containers { | 	for _, container := range containers { | ||||||
| 		// Inspect the container |  | ||||||
| 		inspect, err := docker.InspectContainer(container.ID) | 		inspect, err := docker.InspectContainer(container.ID) | ||||||
|  |  | ||||||
| 		// If there is an error, return false |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return types.TinyauthLabels{}, err | 			log.Warn().Str("id", container.ID).Err(err).Msg("Error inspecting container, skipping") | ||||||
|  | 			continue | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Get the container name (for some reason it is /name) | 		log.Debug().Str("id", inspect.ID).Msg("Getting labels for container") | ||||||
| 		containerName := strings.TrimPrefix(inspect.Name, "/") |  | ||||||
|  |  | ||||||
| 		// There is a container with the same name as the app ID | 		labels, err := utils.GetLabels(inspect.Config.Labels) | ||||||
| 		if containerName == appId { | 		if err != nil { | ||||||
| 			log.Debug().Str("container", containerName).Msg("Found container") | 			log.Warn().Str("id", container.ID).Err(err).Msg("Error getting container labels, skipping") | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 			// Get only the tinyauth labels in a struct | 		// Check if the container matches the ID or domain | ||||||
| 			labels := utils.GetTinyauthLabels(inspect.Config.Labels) | 		for _, lDomain := range labels.Domain { | ||||||
|  | 			if lDomain == domain { | ||||||
|  | 				log.Debug().Str("id", inspect.ID).Msg("Found matching container by domain") | ||||||
|  | 				return labels, nil | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 			log.Debug().Msg("Got labels") | 		if strings.TrimPrefix(inspect.Name, "/") == app { | ||||||
|  | 			log.Debug().Str("id", inspect.ID).Msg("Found matching container by name") | ||||||
| 			// Return labels |  | ||||||
| 			return labels, nil | 			return labels, nil | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Debug().Msg("No matching container found, returning empty labels") | 	log.Debug().Msg("No matching container found, returning empty labels") | ||||||
|  | 	return types.Labels{}, nil | ||||||
| 	// If no matching container is found, return empty labels |  | ||||||
| 	return types.TinyauthLabels{}, nil |  | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										64
									
								
								internal/handlers/context.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								internal/handlers/context.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | |||||||
|  | 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) | ||||||
|  | } | ||||||
| @@ -1,23 +1,23 @@ | |||||||
| package handlers | package handlers | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" |  | ||||||
| 	"net/http" |  | ||||||
| 	"strings" |  | ||||||
| 	"time" |  | ||||||
| 	"tinyauth/internal/auth" | 	"tinyauth/internal/auth" | ||||||
| 	"tinyauth/internal/docker" | 	"tinyauth/internal/docker" | ||||||
| 	"tinyauth/internal/hooks" | 	"tinyauth/internal/hooks" | ||||||
| 	"tinyauth/internal/providers" | 	"tinyauth/internal/providers" | ||||||
| 	"tinyauth/internal/types" | 	"tinyauth/internal/types" | ||||||
| 	"tinyauth/internal/utils" |  | ||||||
|  |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/google/go-querystring/query" |  | ||||||
| 	"github.com/pquerna/otp/totp" |  | ||||||
| 	"github.com/rs/zerolog/log" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | 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 { | func NewHandlers(config types.HandlersConfig, auth *auth.Auth, hooks *hooks.Hooks, providers *providers.Providers, docker *docker.Docker) *Handlers { | ||||||
| 	return &Handlers{ | 	return &Handlers{ | ||||||
| 		Config:    config, | 		Config:    config, | ||||||
| @@ -28,746 +28,6 @@ func NewHandlers(config types.HandlersConfig, auth *auth.Auth, hooks *hooks.Hook | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| type Handlers struct { |  | ||||||
| 	Config    types.HandlersConfig |  | ||||||
| 	Auth      *auth.Auth |  | ||||||
| 	Hooks     *hooks.Hooks |  | ||||||
| 	Providers *providers.Providers |  | ||||||
| 	Docker    *docker.Docker |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (h *Handlers) AuthHandler(c *gin.Context) { |  | ||||||
| 	// Create struct for proxy |  | ||||||
| 	var proxy types.Proxy |  | ||||||
|  |  | ||||||
| 	// Bind URI |  | ||||||
| 	err := c.BindUri(&proxy) |  | ||||||
|  |  | ||||||
| 	// Handle error |  | ||||||
| 	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") |  | ||||||
|  |  | ||||||
| 	// Get headers |  | ||||||
| 	uri := c.Request.Header.Get("X-Forwarded-Uri") |  | ||||||
| 	proto := c.Request.Header.Get("X-Forwarded-Proto") |  | ||||||
| 	host := c.Request.Header.Get("X-Forwarded-Host") |  | ||||||
|  |  | ||||||
| 	// Get the app id |  | ||||||
| 	appId := strings.Split(host, ".")[0] |  | ||||||
|  |  | ||||||
| 	// Get the container labels |  | ||||||
| 	labels, err := h.Docker.GetLabels(appId) |  | ||||||
|  |  | ||||||
| 	log.Debug().Interface("labels", labels).Msg("Got labels") |  | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	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 |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Check if auth is enabled |  | ||||||
| 	authEnabled, err := h.Auth.AuthEnabled(c, labels) |  | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	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 auth is not enabled, return 200 |  | ||||||
| 	if !authEnabled { |  | ||||||
| 		for key, value := range labels.Headers { |  | ||||||
| 			log.Debug().Str("key", key).Str("value", value).Msg("Setting header") |  | ||||||
| 			c.Header(key, utils.SanitizeHeader(value)) |  | ||||||
| 		} |  | ||||||
| 		c.JSON(200, gin.H{ |  | ||||||
| 			"status":  200, |  | ||||||
| 			"message": "Authenticated", |  | ||||||
| 		}) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Get user context |  | ||||||
| 	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 |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Check if user is logged in |  | ||||||
| 	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") |  | ||||||
|  |  | ||||||
| 		// The user is not allowed to access the app |  | ||||||
| 		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 |  | ||||||
| 			values := types.UnauthorizedQuery{ |  | ||||||
| 				Resource: strings.Split(host, ".")[0], |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			// Use either username or email |  | ||||||
| 			if userContext.OAuth { |  | ||||||
| 				values.Username = userContext.Email |  | ||||||
| 			} else { |  | ||||||
| 				values.Username = userContext.Username |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			// Build query |  | ||||||
| 			queries, err := query.Values(values) |  | ||||||
|  |  | ||||||
| 			// Handle error (no need to check for nginx/headers since we are sure we are using caddy/traefik) |  | ||||||
| 			if err != nil { |  | ||||||
| 				log.Error().Err(err).Msg("Failed to build queries") |  | ||||||
| 				c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			// We are using caddy/traefik so redirect |  | ||||||
| 			c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", h.Config.AppURL, queries.Encode())) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// Check groups if using OAuth |  | ||||||
| 		if userContext.OAuth { |  | ||||||
| 			// Check if user is in required groups |  | ||||||
| 			groupOk := h.Auth.OAuthGroup(c, userContext, labels) |  | ||||||
|  |  | ||||||
| 			log.Debug().Bool("groupOk", groupOk).Msg("Checking if user is in required groups") |  | ||||||
|  |  | ||||||
| 			// The user is not allowed to access the app |  | ||||||
| 			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 |  | ||||||
| 				values := types.UnauthorizedQuery{ |  | ||||||
| 					Resource: strings.Split(host, ".")[0], |  | ||||||
| 					GroupErr: true, |  | ||||||
| 				} |  | ||||||
|  |  | ||||||
| 				// Use either username or email |  | ||||||
| 				if userContext.OAuth { |  | ||||||
| 					values.Username = userContext.Email |  | ||||||
| 				} else { |  | ||||||
| 					values.Username = userContext.Username |  | ||||||
| 				} |  | ||||||
|  |  | ||||||
| 				// Build query |  | ||||||
| 				queries, err := query.Values(values) |  | ||||||
|  |  | ||||||
| 				// Handle error (no need to check for nginx/headers since we are sure we are using caddy/traefik) |  | ||||||
| 				if err != nil { |  | ||||||
| 					log.Error().Err(err).Msg("Failed to build queries") |  | ||||||
| 					c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) |  | ||||||
| 					return |  | ||||||
| 				} |  | ||||||
|  |  | ||||||
| 				// We are using caddy/traefik so redirect |  | ||||||
| 				c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", h.Config.AppURL, queries.Encode())) |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		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)) |  | ||||||
|  |  | ||||||
| 		// Set the rest of the headers |  | ||||||
| 		for key, value := range labels.Headers { |  | ||||||
| 			log.Debug().Str("key", key).Str("value", value).Msg("Setting header") |  | ||||||
| 			c.Header(key, utils.SanitizeHeader(value)) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// The user is allowed to access the app |  | ||||||
| 		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") |  | ||||||
|  |  | ||||||
| 	// Redirect to login |  | ||||||
| 	c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/login?%s", h.Config.AppURL, queries.Encode())) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (h *Handlers) LoginHandler(c *gin.Context) { |  | ||||||
| 	// Create login struct |  | ||||||
| 	var login types.LoginRequest |  | ||||||
|  |  | ||||||
| 	// Bind JSON |  | ||||||
| 	err := c.BindJSON(&login) |  | ||||||
|  |  | ||||||
| 	// Handle error |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Error().Err(err).Msg("Failed to bind JSON") |  | ||||||
| 		c.JSON(400, gin.H{ |  | ||||||
| 			"status":  400, |  | ||||||
| 			"message": "Bad Request", |  | ||||||
| 		}) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Got login request") |  | ||||||
|  |  | ||||||
| 	// Get client IP for rate limiting |  | ||||||
| 	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 |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Get user based on username |  | ||||||
| 	user := h.Auth.GetUser(login.Username) |  | ||||||
|  |  | ||||||
| 	// User does not exist |  | ||||||
| 	if user == nil { |  | ||||||
| 		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.CheckPassword(*user, 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 has totp enabled |  | ||||||
| 	if user.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, |  | ||||||
| 		}) |  | ||||||
|  |  | ||||||
| 		// Stop further processing |  | ||||||
| 		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) { |  | ||||||
| 	// Create totp struct |  | ||||||
| 	var totpReq types.TotpRequest |  | ||||||
|  |  | ||||||
| 	// Bind JSON |  | ||||||
| 	err := c.BindJSON(&totpReq) |  | ||||||
|  |  | ||||||
| 	// Handle error |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Error().Err(err).Msg("Failed to bind JSON") |  | ||||||
| 		c.JSON(400, gin.H{ |  | ||||||
| 			"status":  400, |  | ||||||
| 			"message": "Bad Request", |  | ||||||
| 		}) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Checking totp") |  | ||||||
|  |  | ||||||
| 	// Get user context |  | ||||||
| 	userContext := 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.GetUser(userContext.Username) |  | ||||||
|  |  | ||||||
| 	// Check if user exists |  | ||||||
| 	if user == nil { |  | ||||||
| 		log.Debug().Msg("User not found") |  | ||||||
| 		c.JSON(401, gin.H{ |  | ||||||
| 			"status":  401, |  | ||||||
| 			"message": "Unauthorized", |  | ||||||
| 		}) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Check if totp is correct |  | ||||||
| 	ok := totp.Validate(totpReq.Code, user.TotpSecret) |  | ||||||
|  |  | ||||||
| 	// TOTP is incorrect |  | ||||||
| 	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("Logging out") |  | ||||||
|  |  | ||||||
| 	// Delete session cookie |  | ||||||
| 	h.Auth.DeleteSessionCookie(c) |  | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Cleaning up redirect cookie") |  | ||||||
|  |  | ||||||
| 	// Return logged out |  | ||||||
| 	c.JSON(200, gin.H{ |  | ||||||
| 		"status":  200, |  | ||||||
| 		"message": "Logged out", |  | ||||||
| 	}) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (h *Handlers) AppHandler(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") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Create app context struct |  | ||||||
| 	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, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Return app context |  | ||||||
| 	c.JSON(200, appContext) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (h *Handlers) UserHandler(c *gin.Context) { |  | ||||||
| 	log.Debug().Msg("Getting user context") |  | ||||||
|  |  | ||||||
| 	// Get user context |  | ||||||
| 	userContext := h.Hooks.UseUserContext(c) |  | ||||||
|  |  | ||||||
| 	// Create user context response |  | ||||||
| 	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" |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Return user context |  | ||||||
| 	c.JSON(200, userContextResponse) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (h *Handlers) OauthUrlHandler(c *gin.Context) { |  | ||||||
| 	// Create struct for OAuth request |  | ||||||
| 	var request types.OAuthRequest |  | ||||||
|  |  | ||||||
| 	// Bind URI |  | ||||||
| 	err := c.BindUri(&request) |  | ||||||
|  |  | ||||||
| 	// Handle error |  | ||||||
| 	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) |  | ||||||
|  |  | ||||||
| 	// Provider does not exist |  | ||||||
| 	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) { |  | ||||||
| 	// Create struct for OAuth request |  | ||||||
| 	var providerName types.OAuthRequest |  | ||||||
|  |  | ||||||
| 	// Bind URI |  | ||||||
| 	err := c.BindUri(&providerName) |  | ||||||
|  |  | ||||||
| 	// Handle error |  | ||||||
| 	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) |  | ||||||
|  |  | ||||||
| 	log.Debug().Str("provider", providerName.Provider).Msg("Got provider") |  | ||||||
|  |  | ||||||
| 	// Provider does not exist |  | ||||||
| 	if provider == nil { |  | ||||||
| 		c.Redirect(http.StatusTemporaryRedirect, "/not-found") |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Exchange token (authenticates user) |  | ||||||
| 	_, err = provider.ExchangeToken(code) |  | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Got token") |  | ||||||
|  |  | ||||||
| 	// Handle error |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Error().Err(err).Msg("Failed to exchange token") |  | ||||||
| 		c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Get user |  | ||||||
| 	user, err := h.Providers.GetUser(providerName.Provider) |  | ||||||
|  |  | ||||||
| 	// Handle error |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Error().Msg("Failed to get user") |  | ||||||
| 		c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	log.Debug().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") |  | ||||||
|  |  | ||||||
| 		// Build query |  | ||||||
| 		queries, err := query.Values(types.UnauthorizedQuery{ |  | ||||||
| 			Username: user.Email, |  | ||||||
| 		}) |  | ||||||
|  |  | ||||||
| 		// Handle error |  | ||||||
| 		if err != nil { |  | ||||||
| 			log.Error().Err(err).Msg("Failed to build queries") |  | ||||||
| 			c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// Redirect to unauthorized |  | ||||||
| 		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 (also cleans up redirect cookie) |  | ||||||
| 	h.Auth.CreateSessionCookie(c, &types.SessionCookie{ |  | ||||||
| 		Username:    username, |  | ||||||
| 		Name:        name, |  | ||||||
| 		Email:       user.Email, |  | ||||||
| 		Provider:    providerName.Provider, |  | ||||||
| 		OAuthGroups: strings.Join(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") |  | ||||||
|  |  | ||||||
| 	// Build query |  | ||||||
| 	queries, err := query.Values(types.LoginQuery{ |  | ||||||
| 		RedirectURI: redirectCookie, |  | ||||||
| 	}) |  | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Got redirect query") |  | ||||||
|  |  | ||||||
| 	// Handle error |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Error().Err(err).Msg("Failed to build queries") |  | ||||||
| 		c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// 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())) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (h *Handlers) HealthcheckHandler(c *gin.Context) { | func (h *Handlers) HealthcheckHandler(c *gin.Context) { | ||||||
| 	c.JSON(200, gin.H{ | 	c.JSON(200, gin.H{ | ||||||
| 		"status":  200, | 		"status":  200, | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| package api_test | package handlers_test | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| @@ -8,24 +8,26 @@ import ( | |||||||
| 	"reflect" | 	"reflect" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
| 	"tinyauth/internal/api" | 	"time" | ||||||
| 	"tinyauth/internal/auth" | 	"tinyauth/internal/auth" | ||||||
| 	"tinyauth/internal/docker" | 	"tinyauth/internal/docker" | ||||||
| 	"tinyauth/internal/handlers" | 	"tinyauth/internal/handlers" | ||||||
| 	"tinyauth/internal/hooks" | 	"tinyauth/internal/hooks" | ||||||
| 	"tinyauth/internal/providers" | 	"tinyauth/internal/providers" | ||||||
|  | 	"tinyauth/internal/server" | ||||||
| 	"tinyauth/internal/types" | 	"tinyauth/internal/types" | ||||||
| 
 | 
 | ||||||
| 	"github.com/magiconair/properties/assert" | 	"github.com/magiconair/properties/assert" | ||||||
|  | 	"github.com/pquerna/otp/totp" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Simple API config for tests | // Simple server config | ||||||
| var apiConfig = types.APIConfig{ | var serverConfig = types.ServerConfig{ | ||||||
| 	Port:    8080, | 	Port:    8080, | ||||||
| 	Address: "0.0.0.0", | 	Address: "0.0.0.0", | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Simple handlers config for tests | // Simple handlers config | ||||||
| var handlersConfig = types.HandlersConfig{ | var handlersConfig = types.HandlersConfig{ | ||||||
| 	AppURL:                "http://localhost:8080", | 	AppURL:                "http://localhost:8080", | ||||||
| 	Domain:                "localhost", | 	Domain:                "localhost", | ||||||
| @@ -33,18 +35,19 @@ var handlersConfig = types.HandlersConfig{ | |||||||
| 	CookieSecure:          false, | 	CookieSecure:          false, | ||||||
| 	Title:                 "Tinyauth", | 	Title:                 "Tinyauth", | ||||||
| 	GenericName:           "Generic", | 	GenericName:           "Generic", | ||||||
| 	ForgotPasswordMessage: "Some message", | 	ForgotPasswordMessage: "Message", | ||||||
| 	CsrfCookieName:        "tinyauth-csrf", | 	CsrfCookieName:        "tinyauth-csrf", | ||||||
| 	RedirectCookieName:    "tinyauth-redirect", | 	RedirectCookieName:    "tinyauth-redirect", | ||||||
| 	BackgroundImage:       "https://example.com/image.png", | 	BackgroundImage:       "https://example.com/image.png", | ||||||
| 	OAuthAutoRedirect:     "none", | 	OAuthAutoRedirect:     "none", | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Simple auth config for tests | // Simple auth config | ||||||
| var authConfig = types.AuthConfig{ | var authConfig = types.AuthConfig{ | ||||||
| 	Users:             types.Users{}, | 	Users:             types.Users{}, | ||||||
| 	OauthWhitelist:    "", | 	OauthWhitelist:    "", | ||||||
| 	Secret:            "super-secret-api-thing-for-tests", // It is 32 chars long | 	HMACSecret:        "4bZ9K.*:;zH=,9zG!meUxu.B5-S[7.V.", // Complex on purpose | ||||||
|  | 	EncryptionSecret:  "\\:!R(u[Sbv6ZLm.7es)H|OqH4y}0u\\rj", | ||||||
| 	CookieSecure:      false, | 	CookieSecure:      false, | ||||||
| 	SessionExpiry:     3600, | 	SessionExpiry:     3600, | ||||||
| 	LoginTimeout:      0, | 	LoginTimeout:      0, | ||||||
| @@ -53,7 +56,7 @@ var authConfig = types.AuthConfig{ | |||||||
| 	Domain:            "localhost", | 	Domain:            "localhost", | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Simple hooks config for tests | // Simple hooks config | ||||||
| var hooksConfig = types.HooksConfig{ | var hooksConfig = types.HooksConfig{ | ||||||
| 	Domain: "localhost", | 	Domain: "localhost", | ||||||
| } | } | ||||||
| @@ -67,145 +70,105 @@ var user = types.User{ | |||||||
| 	Password: "$2a$10$AvGHLTYv3xiRJ0xV9xs3XeVIlkGTygI9nqIamFYB5Xu.5.0UWF7B6", // pass | 	Password: "$2a$10$AvGHLTYv3xiRJ0xV9xs3XeVIlkGTygI9nqIamFYB5Xu.5.0UWF7B6", // pass | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // We need all this to be able to test the API | // Initialize the server for tests | ||||||
| func getAPI(t *testing.T) *api.API { | func getServer(t *testing.T) *server.Server { | ||||||
| 	// Create docker service | 	// Create services | ||||||
| 	docker := docker.NewDocker() |  | ||||||
| 
 |  | ||||||
| 	// Initialize docker |  | ||||||
| 	err := docker.Init() |  | ||||||
| 
 |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatalf("Failed to initialize docker: %v", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Create auth service |  | ||||||
| 	authConfig.Users = types.Users{ | 	authConfig.Users = types.Users{ | ||||||
| 		{ | 		{ | ||||||
| 			Username: user.Username, | 			Username:   user.Username, | ||||||
| 			Password: user.Password, | 			Password:   user.Password, | ||||||
|  | 			TotpSecret: user.TotpSecret, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| 	auth := auth.NewAuth(authConfig, docker) | 	docker, err := docker.NewDocker() | ||||||
| 
 | 	if err != nil { | ||||||
| 	// Create providers service | 		t.Fatalf("Failed to create docker client: %v", err) | ||||||
|  | 	} | ||||||
|  | 	auth := auth.NewAuth(authConfig, nil, nil) | ||||||
| 	providers := providers.NewProviders(types.OAuthConfig{}) | 	providers := providers.NewProviders(types.OAuthConfig{}) | ||||||
| 
 |  | ||||||
| 	// Initialize providers |  | ||||||
| 	providers.Init() |  | ||||||
| 
 |  | ||||||
| 	// Create hooks service |  | ||||||
| 	hooks := hooks.NewHooks(hooksConfig, auth, providers) | 	hooks := hooks.NewHooks(hooksConfig, auth, providers) | ||||||
| 
 |  | ||||||
| 	// Create handlers service |  | ||||||
| 	handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker) | 	handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker) | ||||||
| 
 | 
 | ||||||
| 	// Create API | 	// Create server | ||||||
| 	api := api.NewAPI(apiConfig, handlers) | 	srv, err := server.NewServer(serverConfig, handlers) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Failed to create server: %v", err) | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	// Setup routes | 	return srv | ||||||
| 	api.Init() |  | ||||||
| 	api.SetupRoutes() |  | ||||||
| 
 |  | ||||||
| 	return api |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Test login (we will need this for the other tests) |  | ||||||
| func TestLogin(t *testing.T) { | func TestLogin(t *testing.T) { | ||||||
| 	t.Log("Testing login") | 	t.Log("Testing login") | ||||||
| 
 | 
 | ||||||
| 	// Get API | 	srv := getServer(t) | ||||||
| 	api := getAPI(t) |  | ||||||
| 
 | 
 | ||||||
| 	// Create recorder |  | ||||||
| 	recorder := httptest.NewRecorder() | 	recorder := httptest.NewRecorder() | ||||||
| 
 | 
 | ||||||
| 	// Create request |  | ||||||
| 	user := types.LoginRequest{ | 	user := types.LoginRequest{ | ||||||
| 		Username: "user", | 		Username: "user", | ||||||
| 		Password: "pass", | 		Password: "pass", | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	json, err := json.Marshal(user) | 	json, err := json.Marshal(user) | ||||||
| 
 |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error marshalling json: %v", err) | 		t.Fatalf("Error marshalling json: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Create request |  | ||||||
| 	req, err := http.NewRequest("POST", "/api/login", strings.NewReader(string(json))) | 	req, err := http.NewRequest("POST", "/api/login", strings.NewReader(string(json))) | ||||||
| 
 |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error creating request: %v", err) | 		t.Fatalf("Error creating request: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Serve the request | 	srv.Router.ServeHTTP(recorder, req) | ||||||
| 	api.Router.ServeHTTP(recorder, req) |  | ||||||
| 
 |  | ||||||
| 	// Assert |  | ||||||
| 	assert.Equal(t, recorder.Code, http.StatusOK) | 	assert.Equal(t, recorder.Code, http.StatusOK) | ||||||
| 
 | 
 | ||||||
| 	// Get the cookie | 	cookies := recorder.Result().Cookies() | ||||||
| 	cookie = recorder.Result().Cookies()[0].Value |  | ||||||
| 
 | 
 | ||||||
| 	// Check if the cookie is set | 	if len(cookies) == 0 { | ||||||
| 	if cookie == "" { |  | ||||||
| 		t.Fatalf("Cookie not set") | 		t.Fatalf("Cookie not set") | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	// Set the cookie for further tests | ||||||
|  | 	cookie = cookies[0].Value | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Test app context |  | ||||||
| func TestAppContext(t *testing.T) { | func TestAppContext(t *testing.T) { | ||||||
|  | 	// Refresh the cookie | ||||||
|  | 	TestLogin(t) | ||||||
|  | 
 | ||||||
| 	t.Log("Testing app context") | 	t.Log("Testing app context") | ||||||
| 
 | 
 | ||||||
| 	// Get API | 	srv := getServer(t) | ||||||
| 	api := getAPI(t) |  | ||||||
| 
 | 
 | ||||||
| 	// Create recorder |  | ||||||
| 	recorder := httptest.NewRecorder() | 	recorder := httptest.NewRecorder() | ||||||
| 
 | 
 | ||||||
| 	// Create request |  | ||||||
| 	req, err := http.NewRequest("GET", "/api/app", nil) | 	req, err := http.NewRequest("GET", "/api/app", nil) | ||||||
| 
 |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error creating request: %v", err) | 		t.Fatalf("Error creating request: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Set the cookie | 	// Set the cookie from the previous test | ||||||
| 	req.AddCookie(&http.Cookie{ | 	req.AddCookie(&http.Cookie{ | ||||||
| 		Name:  "tinyauth", | 		Name:  "tinyauth", | ||||||
| 		Value: cookie, | 		Value: cookie, | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	// Serve the request | 	srv.Router.ServeHTTP(recorder, req) | ||||||
| 	api.Router.ServeHTTP(recorder, req) |  | ||||||
| 
 |  | ||||||
| 	// Assert |  | ||||||
| 	assert.Equal(t, recorder.Code, http.StatusOK) | 	assert.Equal(t, recorder.Code, http.StatusOK) | ||||||
| 
 | 
 | ||||||
| 	// Read the body of the response |  | ||||||
| 	body, err := io.ReadAll(recorder.Body) | 	body, err := io.ReadAll(recorder.Body) | ||||||
| 
 |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error getting body: %v", err) | 		t.Fatalf("Error getting body: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Unmarshal the body into the user struct |  | ||||||
| 	var app types.AppContext | 	var app types.AppContext | ||||||
| 
 | 
 | ||||||
| 	err = json.Unmarshal(body, &app) | 	err = json.Unmarshal(body, &app) | ||||||
| 
 |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error unmarshalling body: %v", err) | 		t.Fatalf("Error unmarshalling body: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Create tests values |  | ||||||
| 	expected := types.AppContext{ | 	expected := types.AppContext{ | ||||||
| 		Status:                200, | 		Status:                200, | ||||||
| 		Message:               "OK", | 		Message:               "OK", | ||||||
| @@ -213,7 +176,7 @@ func TestAppContext(t *testing.T) { | |||||||
| 		DisableContinue:       false, | 		DisableContinue:       false, | ||||||
| 		Title:                 "Tinyauth", | 		Title:                 "Tinyauth", | ||||||
| 		GenericName:           "Generic", | 		GenericName:           "Generic", | ||||||
| 		ForgotPasswordMessage: "Some message", | 		ForgotPasswordMessage: "Message", | ||||||
| 		BackgroundImage:       "https://example.com/image.png", | 		BackgroundImage:       "https://example.com/image.png", | ||||||
| 		OAuthAutoRedirect:     "none", | 		OAuthAutoRedirect:     "none", | ||||||
| 		Domain:                "localhost", | 		Domain:                "localhost", | ||||||
| @@ -225,45 +188,34 @@ func TestAppContext(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Test user context |  | ||||||
| func TestUserContext(t *testing.T) { | func TestUserContext(t *testing.T) { | ||||||
|  | 	// Refresh the cookie | ||||||
|  | 	TestLogin(t) | ||||||
|  | 
 | ||||||
| 	t.Log("Testing user context") | 	t.Log("Testing user context") | ||||||
| 
 | 
 | ||||||
| 	// Get API | 	srv := getServer(t) | ||||||
| 	api := getAPI(t) |  | ||||||
| 
 | 
 | ||||||
| 	// Create recorder |  | ||||||
| 	recorder := httptest.NewRecorder() | 	recorder := httptest.NewRecorder() | ||||||
| 
 | 
 | ||||||
| 	// Create request |  | ||||||
| 	req, err := http.NewRequest("GET", "/api/user", nil) | 	req, err := http.NewRequest("GET", "/api/user", nil) | ||||||
| 
 |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error creating request: %v", err) | 		t.Fatalf("Error creating request: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Set the cookie |  | ||||||
| 	req.AddCookie(&http.Cookie{ | 	req.AddCookie(&http.Cookie{ | ||||||
| 		Name:  "tinyauth-session", | 		Name:  "tinyauth-session", | ||||||
| 		Value: cookie, | 		Value: cookie, | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	// Serve the request | 	srv.Router.ServeHTTP(recorder, req) | ||||||
| 	api.Router.ServeHTTP(recorder, req) |  | ||||||
| 
 |  | ||||||
| 	// Assert |  | ||||||
| 	assert.Equal(t, recorder.Code, http.StatusOK) | 	assert.Equal(t, recorder.Code, http.StatusOK) | ||||||
| 
 | 
 | ||||||
| 	// Read the body of the response |  | ||||||
| 	body, err := io.ReadAll(recorder.Body) | 	body, err := io.ReadAll(recorder.Body) | ||||||
| 
 |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error getting body: %v", err) | 		t.Fatalf("Error getting body: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Unmarshal the body into the user struct |  | ||||||
| 	type User struct { | 	type User struct { | ||||||
| 		Username string `json:"username"` | 		Username string `json:"username"` | ||||||
| 	} | 	} | ||||||
| @@ -271,52 +223,172 @@ func TestUserContext(t *testing.T) { | |||||||
| 	var user User | 	var user User | ||||||
| 
 | 
 | ||||||
| 	err = json.Unmarshal(body, &user) | 	err = json.Unmarshal(body, &user) | ||||||
| 
 |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error unmarshalling body: %v", err) | 		t.Fatalf("Error unmarshalling body: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// We should get the username back | 	// We should get the user back | ||||||
| 	if user.Username != "user" { | 	if user.Username != "user" { | ||||||
| 		t.Fatalf("Expected user, got %s", user.Username) | 		t.Fatalf("Expected user, got %s", user.Username) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Test logout |  | ||||||
| func TestLogout(t *testing.T) { | func TestLogout(t *testing.T) { | ||||||
|  | 	// Refresh the cookie | ||||||
|  | 	TestLogin(t) | ||||||
|  | 
 | ||||||
| 	t.Log("Testing logout") | 	t.Log("Testing logout") | ||||||
| 
 | 
 | ||||||
| 	// Get API | 	srv := getServer(t) | ||||||
| 	api := getAPI(t) |  | ||||||
| 
 | 
 | ||||||
| 	// Create recorder |  | ||||||
| 	recorder := httptest.NewRecorder() | 	recorder := httptest.NewRecorder() | ||||||
| 
 | 
 | ||||||
| 	// Create request |  | ||||||
| 	req, err := http.NewRequest("POST", "/api/logout", nil) | 	req, err := http.NewRequest("POST", "/api/logout", nil) | ||||||
| 
 |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error creating request: %v", err) | 		t.Fatalf("Error creating request: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Set the cookie |  | ||||||
| 	req.AddCookie(&http.Cookie{ | 	req.AddCookie(&http.Cookie{ | ||||||
| 		Name:  "tinyauth", | 		Name:  "tinyauth-session", | ||||||
| 		Value: cookie, | 		Value: cookie, | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	// Serve the request | 	srv.Router.ServeHTTP(recorder, req) | ||||||
| 	api.Router.ServeHTTP(recorder, req) |  | ||||||
| 
 |  | ||||||
| 	// Assert |  | ||||||
| 	assert.Equal(t, recorder.Code, http.StatusOK) | 	assert.Equal(t, recorder.Code, http.StatusOK) | ||||||
| 
 | 
 | ||||||
| 	// Check if the cookie is different (means go sessions flushed it) | 	// Check if the cookie is different (means the cookie is gone) | ||||||
| 	if recorder.Result().Cookies()[0].Value == cookie { | 	if recorder.Result().Cookies()[0].Value == cookie { | ||||||
| 		t.Fatalf("Cookie not flushed") | 		t.Fatalf("Cookie not flushed") | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // TODO: Testing for the oauth stuff | 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) | ||||||
|  | } | ||||||
							
								
								
									
										223
									
								
								internal/handlers/oauth.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								internal/handlers/oauth.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,223 @@ | |||||||
|  | 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())) | ||||||
|  | } | ||||||
							
								
								
									
										282
									
								
								internal/handlers/proxy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										282
									
								
								internal/handlers/proxy.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,282 @@ | |||||||
|  | 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())) | ||||||
|  | } | ||||||
							
								
								
									
										197
									
								
								internal/handlers/user.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										197
									
								
								internal/handlers/user.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,197 @@ | |||||||
|  | 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", | ||||||
|  | 	}) | ||||||
|  | } | ||||||
| @@ -4,6 +4,7 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"tinyauth/internal/auth" | 	"tinyauth/internal/auth" | ||||||
|  | 	"tinyauth/internal/oauth" | ||||||
| 	"tinyauth/internal/providers" | 	"tinyauth/internal/providers" | ||||||
| 	"tinyauth/internal/types" | 	"tinyauth/internal/types" | ||||||
| 	"tinyauth/internal/utils" | 	"tinyauth/internal/utils" | ||||||
| @@ -12,6 +13,12 @@ import ( | |||||||
| 	"github.com/rs/zerolog/log" | 	"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 { | func NewHooks(config types.HooksConfig, auth *auth.Auth, providers *providers.Providers) *Hooks { | ||||||
| 	return &Hooks{ | 	return &Hooks{ | ||||||
| 		Config:    config, | 		Config:    config, | ||||||
| @@ -20,58 +27,17 @@ func NewHooks(config types.HooksConfig, auth *auth.Auth, providers *providers.Pr | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| type Hooks struct { |  | ||||||
| 	Config    types.HooksConfig |  | ||||||
| 	Auth      *auth.Auth |  | ||||||
| 	Providers *providers.Providers |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { | func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { | ||||||
| 	// Get session cookie and basic auth |  | ||||||
| 	cookie, err := hooks.Auth.GetSessionCookie(c) | 	cookie, err := hooks.Auth.GetSessionCookie(c) | ||||||
| 	basic := hooks.Auth.GetBasicAuth(c) | 	var provider *oauth.OAuth | ||||||
|  |  | ||||||
| 	// Check if basic auth is set |  | ||||||
| 	if basic != nil { |  | ||||||
| 		log.Debug().Msg("Got basic auth") |  | ||||||
|  |  | ||||||
| 		// Get user |  | ||||||
| 		user := hooks.Auth.GetUser(basic.Username) |  | ||||||
|  |  | ||||||
| 		// Check we have a user |  | ||||||
| 		if user == nil { |  | ||||||
| 			log.Error().Str("username", basic.Username).Msg("User does not exist") |  | ||||||
|  |  | ||||||
| 			// Return empty context |  | ||||||
| 			return types.UserContext{} |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// Check if the user has a correct password |  | ||||||
| 		if hooks.Auth.CheckPassword(*user, basic.Password) { |  | ||||||
| 			// Return user context since we are logged in with basic auth |  | ||||||
| 			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 != "", |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Check cookie error after basic auth |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error().Err(err).Msg("Failed to get session cookie") | 		log.Error().Err(err).Msg("Failed to get session cookie") | ||||||
| 		// Return empty context | 		goto basic | ||||||
| 		return types.UserContext{} |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Check if session cookie has totp pending |  | ||||||
| 	if cookie.TotpPending { | 	if cookie.TotpPending { | ||||||
| 		log.Debug().Msg("Totp pending") | 		log.Debug().Msg("Totp pending") | ||||||
| 		// Return empty context since we are pending totp |  | ||||||
| 		return types.UserContext{ | 		return types.UserContext{ | ||||||
| 			Username:    cookie.Username, | 			Username:    cookie.Username, | ||||||
| 			Name:        cookie.Name, | 			Name:        cookie.Name, | ||||||
| @@ -81,48 +47,42 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Check if session cookie is username/password auth |  | ||||||
| 	if cookie.Provider == "username" { | 	if cookie.Provider == "username" { | ||||||
| 		log.Debug().Msg("Provider is username") | 		log.Debug().Msg("Provider is username") | ||||||
|  |  | ||||||
| 		// Check if user exists | 		userSearch := hooks.Auth.SearchUser(cookie.Username) | ||||||
| 		if hooks.Auth.GetUser(cookie.Username) != nil { |  | ||||||
| 			log.Debug().Msg("User exists") |  | ||||||
|  |  | ||||||
| 			// It exists so we are logged in | 		if userSearch.Type == "unknown" { | ||||||
| 			return types.UserContext{ | 			log.Warn().Str("username", cookie.Username).Msg("User does not exist") | ||||||
| 				Username:   cookie.Username, | 			goto basic | ||||||
| 				Name:       cookie.Name, | 		} | ||||||
| 				Email:      cookie.Email, |  | ||||||
| 				IsLoggedIn: true, | 		log.Debug().Str("type", userSearch.Type).Msg("User exists") | ||||||
| 				Provider:   "username", |  | ||||||
| 			} | 		return types.UserContext{ | ||||||
|  | 			Username:   cookie.Username, | ||||||
|  | 			Name:       cookie.Name, | ||||||
|  | 			Email:      cookie.Email, | ||||||
|  | 			IsLoggedIn: true, | ||||||
|  | 			Provider:   "username", | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Provider is not username") | 	log.Debug().Msg("Provider is not username") | ||||||
|  |  | ||||||
| 	// The provider is not username so we need to check if it is an oauth provider | 	provider = hooks.Providers.GetProvider(cookie.Provider) | ||||||
| 	provider := hooks.Providers.GetProvider(cookie.Provider) |  | ||||||
|  |  | ||||||
| 	// If we have a provider with this name |  | ||||||
| 	if provider != nil { | 	if provider != nil { | ||||||
| 		log.Debug().Msg("Provider exists") | 		log.Debug().Msg("Provider exists") | ||||||
|  |  | ||||||
| 		// Check if the oauth email is whitelisted |  | ||||||
| 		if !hooks.Auth.EmailWhitelisted(cookie.Email) { | 		if !hooks.Auth.EmailWhitelisted(cookie.Email) { | ||||||
| 			log.Error().Str("email", cookie.Email).Msg("Email is not whitelisted") | 			log.Warn().Str("email", cookie.Email).Msg("Email is not whitelisted") | ||||||
|  |  | ||||||
| 			// It isn't so we delete the cookie and return an empty context |  | ||||||
| 			hooks.Auth.DeleteSessionCookie(c) | 			hooks.Auth.DeleteSessionCookie(c) | ||||||
|  | 			goto basic | ||||||
| 			// Return empty context |  | ||||||
| 			return types.UserContext{} |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		log.Debug().Msg("Email is whitelisted") | 		log.Debug().Msg("Email is whitelisted") | ||||||
|  |  | ||||||
| 		// Return user context since we are logged in with oauth |  | ||||||
| 		return types.UserContext{ | 		return types.UserContext{ | ||||||
| 			Username:    cookie.Username, | 			Username:    cookie.Username, | ||||||
| 			Name:        cookie.Name, | 			Name:        cookie.Name, | ||||||
| @@ -134,6 +94,51 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Neither basic auth or oauth is set so we return an empty context | 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{} | 	return types.UserContext{} | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										147
									
								
								internal/ldap/ldap.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								internal/ldap/ldap.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | |||||||
|  | package ldap | ||||||
|  |  | ||||||
|  | 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 | ||||||
|  | 	Conn   *ldapgo.Conn | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NewLDAP(config types.LdapConfig) (*LDAP, error) { | ||||||
|  | 	ldap := &LDAP{ | ||||||
|  | 		Config: config, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	_, err := ldap.connect() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to connect to LDAP server: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	go func() { | ||||||
|  | 		for range time.Tick(time.Duration(5) * time.Minute) { | ||||||
|  | 			err := ldap.heartbeat() | ||||||
|  | 			if err != nil { | ||||||
|  | 				log.Error().Err(err).Msg("LDAP connection heartbeat failed") | ||||||
|  | 				if reconnectErr := ldap.reconnect(); reconnectErr != nil { | ||||||
|  | 					log.Error().Err(reconnectErr).Msg("Failed to reconnect to LDAP server") | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 				log.Info().Msg("Successfully reconnected to LDAP server") | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	return ldap, 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, | ||||||
|  | 		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) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Set and return the connection | ||||||
|  | 	l.Conn = conn | ||||||
|  | 	return conn, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (l *LDAP) Search(username string) (string, error) { | ||||||
|  | 	// Escape the username to prevent LDAP injection | ||||||
|  | 	escapedUsername := ldapgo.EscapeFilter(username) | ||||||
|  | 	filter := fmt.Sprintf(l.Config.SearchFilter, escapedUsername) | ||||||
|  |  | ||||||
|  | 	searchRequest := ldapgo.NewSearchRequest( | ||||||
|  | 		l.Config.BaseDN, | ||||||
|  | 		ldapgo.ScopeWholeSubtree, ldapgo.NeverDerefAliases, 0, 0, false, | ||||||
|  | 		filter, | ||||||
|  | 		[]string{"dn"}, | ||||||
|  | 		nil, | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	searchResult, err := l.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) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	userDN := searchResult.Entries[0].DN | ||||||
|  | 	return userDN, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (l *LDAP) Bind(userDN string, password string) error { | ||||||
|  | 	err := l.Conn.Bind(userDN, password) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (l *LDAP) heartbeat() error { | ||||||
|  | 	log.Debug().Msg("Performing LDAP connection heartbeat") | ||||||
|  |  | ||||||
|  | 	searchRequest := ldapgo.NewSearchRequest( | ||||||
|  | 		"", | ||||||
|  | 		ldapgo.ScopeBaseObject, ldapgo.NeverDerefAliases, 0, 0, false, | ||||||
|  | 		"(objectClass=*)", | ||||||
|  | 		[]string{}, | ||||||
|  | 		nil, | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	_, err := l.Conn.Search(searchRequest) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// No error means the connection is alive | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (l *LDAP) reconnect() error { | ||||||
|  | 	log.Info().Msg("Reconnecting to LDAP server") | ||||||
|  |  | ||||||
|  | 	exp := backoff.NewExponentialBackOff() | ||||||
|  | 	exp.InitialInterval = 500 * time.Millisecond | ||||||
|  | 	exp.RandomizationFactor = 0.1 | ||||||
|  | 	exp.Multiplier = 1.5 | ||||||
|  | 	exp.Reset() | ||||||
|  |  | ||||||
|  | 	operation := func() (*ldapgo.Conn, error) { | ||||||
|  | 		l.Conn.Close() | ||||||
|  | 		conn, err := l.connect() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, nil | ||||||
|  | 		} | ||||||
|  | 		return conn, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	_, err := backoff.Retry(context.TODO(), operation, backoff.WithBackOff(exp), backoff.WithMaxTries(3)) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
| @@ -10,79 +10,62 @@ import ( | |||||||
| 	"golang.org/x/oauth2" | 	"golang.org/x/oauth2" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func NewOAuth(config oauth2.Config, insecureSkipVerify bool) *OAuth { |  | ||||||
| 	return &OAuth{ |  | ||||||
| 		Config:             config, |  | ||||||
| 		InsecureSkipVerify: insecureSkipVerify, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type OAuth struct { | type OAuth struct { | ||||||
| 	Config             oauth2.Config | 	Config   oauth2.Config | ||||||
| 	Context            context.Context | 	Context  context.Context | ||||||
| 	Token              *oauth2.Token | 	Token    *oauth2.Token | ||||||
| 	Verifier           string | 	Verifier string | ||||||
| 	InsecureSkipVerify bool |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func (oauth *OAuth) Init() { | func NewOAuth(config oauth2.Config, insecureSkipVerify bool) *OAuth { | ||||||
| 	// Create transport with TLS |  | ||||||
| 	transport := &http.Transport{ | 	transport := &http.Transport{ | ||||||
| 		TLSClientConfig: &tls.Config{ | 		TLSClientConfig: &tls.Config{ | ||||||
| 			InsecureSkipVerify: oauth.InsecureSkipVerify, | 			InsecureSkipVerify: insecureSkipVerify, | ||||||
| 			MinVersion:         tls.VersionTLS12, | 			MinVersion:         tls.VersionTLS12, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Create a new context |  | ||||||
| 	oauth.Context = context.Background() |  | ||||||
|  |  | ||||||
| 	// Create the HTTP client with the transport |  | ||||||
| 	httpClient := &http.Client{ | 	httpClient := &http.Client{ | ||||||
| 		Transport: transport, | 		Transport: transport, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	ctx := context.Background() | ||||||
|  |  | ||||||
| 	// Set the HTTP client in the context | 	// Set the HTTP client in the context | ||||||
| 	oauth.Context = context.WithValue(oauth.Context, oauth2.HTTPClient, httpClient) | 	ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) | ||||||
| 	// Create the verifier |  | ||||||
| 	oauth.Verifier = oauth2.GenerateVerifier() | 	verifier := oauth2.GenerateVerifier() | ||||||
|  |  | ||||||
|  | 	return &OAuth{ | ||||||
|  | 		Config:   config, | ||||||
|  | 		Context:  ctx, | ||||||
|  | 		Verifier: verifier, | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (oauth *OAuth) GetAuthURL(state string) string { | func (oauth *OAuth) GetAuthURL(state string) string { | ||||||
| 	// Return the auth url |  | ||||||
| 	return oauth.Config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(oauth.Verifier)) | 	return oauth.Config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(oauth.Verifier)) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (oauth *OAuth) ExchangeToken(code string) (string, error) { | func (oauth *OAuth) ExchangeToken(code string) (string, error) { | ||||||
| 	// Exchange the code for a token |  | ||||||
| 	token, err := oauth.Config.Exchange(oauth.Context, code, oauth2.VerifierOption(oauth.Verifier)) | 	token, err := oauth.Config.Exchange(oauth.Context, code, oauth2.VerifierOption(oauth.Verifier)) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return "", err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Set the token | 	// Set and return the token | ||||||
| 	oauth.Token = token | 	oauth.Token = token | ||||||
|  |  | ||||||
| 	// Return the access token |  | ||||||
| 	return oauth.Token.AccessToken, nil | 	return oauth.Token.AccessToken, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (oauth *OAuth) GetClient() *http.Client { | func (oauth *OAuth) GetClient() *http.Client { | ||||||
| 	// Return the http client with the token set |  | ||||||
| 	return oauth.Config.Client(oauth.Context, oauth.Token) | 	return oauth.Config.Client(oauth.Context, oauth.Token) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (oauth *OAuth) GenerateState() string { | func (oauth *OAuth) GenerateState() string { | ||||||
| 	// Generate a random state string |  | ||||||
| 	b := make([]byte, 128) | 	b := make([]byte, 128) | ||||||
|  |  | ||||||
| 	// Fill the byte slice with random data |  | ||||||
| 	rand.Read(b) | 	rand.Read(b) | ||||||
|  |  | ||||||
| 	// Encode the byte slice to a base64 string |  | ||||||
| 	state := base64.URLEncoding.EncodeToString(b) | 	state := base64.URLEncoding.EncodeToString(b) | ||||||
|  |  | ||||||
| 	return state | 	return state | ||||||
| } | } | ||||||
|   | |||||||
| @@ -10,41 +10,28 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| func GetGenericUser(client *http.Client, url string) (constants.Claims, error) { | func GetGenericUser(client *http.Client, url string) (constants.Claims, error) { | ||||||
| 	// Create user struct |  | ||||||
| 	var user constants.Claims | 	var user constants.Claims | ||||||
|  |  | ||||||
| 	// Using the oauth client get the user info url |  | ||||||
| 	res, err := client.Get(url) | 	res, err := client.Get(url) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return user, err | 		return user, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	defer res.Body.Close() | 	defer res.Body.Close() | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Got response from generic provider") | 	log.Debug().Msg("Got response from generic provider") | ||||||
|  |  | ||||||
| 	// Read the body of the response |  | ||||||
| 	body, err := io.ReadAll(res.Body) | 	body, err := io.ReadAll(res.Body) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return user, err | 		return user, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Read body from generic provider") | 	log.Debug().Msg("Read body from generic provider") | ||||||
|  |  | ||||||
| 	// Unmarshal the body into the user struct |  | ||||||
| 	err = json.Unmarshal(body, &user) | 	err = json.Unmarshal(body, &user) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return user, err | 		return user, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Parsed user from generic provider") | 	log.Debug().Msg("Parsed user from generic provider") | ||||||
|  |  | ||||||
| 	// Return the user |  | ||||||
| 	return user, nil | 	return user, nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -28,71 +28,48 @@ func GithubScopes() []string { | |||||||
| } | } | ||||||
|  |  | ||||||
| func GetGithubUser(client *http.Client) (constants.Claims, error) { | func GetGithubUser(client *http.Client) (constants.Claims, error) { | ||||||
| 	// Create user struct |  | ||||||
| 	var user constants.Claims | 	var user constants.Claims | ||||||
|  |  | ||||||
| 	// Get the user info from github using the oauth http client |  | ||||||
| 	res, err := client.Get("https://api.github.com/user") | 	res, err := client.Get("https://api.github.com/user") | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return user, err | 		return user, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	defer res.Body.Close() | 	defer res.Body.Close() | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Got user response from github") | 	log.Debug().Msg("Got user response from github") | ||||||
|  |  | ||||||
| 	// Read the body of the response |  | ||||||
| 	body, err := io.ReadAll(res.Body) | 	body, err := io.ReadAll(res.Body) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return user, err | 		return user, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Read user body from github") | 	log.Debug().Msg("Read user body from github") | ||||||
|  |  | ||||||
| 	// Parse the body into a user struct |  | ||||||
| 	var userInfo GithubUserInfoResponse | 	var userInfo GithubUserInfoResponse | ||||||
|  |  | ||||||
| 	// Unmarshal the body into the user struct |  | ||||||
| 	err = json.Unmarshal(body, &userInfo) | 	err = json.Unmarshal(body, &userInfo) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return user, err | 		return user, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Get the user emails from github using the oauth http client |  | ||||||
| 	res, err = client.Get("https://api.github.com/user/emails") | 	res, err = client.Get("https://api.github.com/user/emails") | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return user, err | 		return user, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	defer res.Body.Close() | 	defer res.Body.Close() | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Got email response from github") | 	log.Debug().Msg("Got email response from github") | ||||||
|  |  | ||||||
| 	// Read the body of the response |  | ||||||
| 	body, err = io.ReadAll(res.Body) | 	body, err = io.ReadAll(res.Body) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return user, err | 		return user, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Read email body from github") | 	log.Debug().Msg("Read email body from github") | ||||||
|  |  | ||||||
| 	// Parse the body into a user struct |  | ||||||
| 	var emails GithubEmailResponse | 	var emails GithubEmailResponse | ||||||
|  |  | ||||||
| 	// Unmarshal the body into the user struct |  | ||||||
| 	err = json.Unmarshal(body, &emails) | 	err = json.Unmarshal(body, &emails) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return user, err | 		return user, err | ||||||
| 	} | 	} | ||||||
| @@ -102,28 +79,24 @@ func GetGithubUser(client *http.Client) (constants.Claims, error) { | |||||||
| 	// Find and return the primary email | 	// Find and return the primary email | ||||||
| 	for _, email := range emails { | 	for _, email := range emails { | ||||||
| 		if email.Primary { | 		if email.Primary { | ||||||
| 			// Set the email then exit |  | ||||||
| 			log.Debug().Str("email", email.Email).Msg("Found primary email") | 			log.Debug().Str("email", email.Email).Msg("Found primary email") | ||||||
| 			user.Email = email.Email | 			user.Email = email.Email | ||||||
| 			break | 			break | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// If no primary email was found, use the first available email |  | ||||||
| 	if len(emails) == 0 { | 	if len(emails) == 0 { | ||||||
| 		return user, errors.New("no emails found") | 		return user, errors.New("no emails found") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Set the email if it is not set picking the first one | 	// Use first available email if no primary email was found | ||||||
| 	if user.Email == "" { | 	if user.Email == "" { | ||||||
| 		log.Warn().Str("email", emails[0].Email).Msg("No primary email found, using first email") | 		log.Warn().Str("email", emails[0].Email).Msg("No primary email found, using first email") | ||||||
| 		user.Email = emails[0].Email | 		user.Email = emails[0].Email | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Set the username and name |  | ||||||
| 	user.PreferredUsername = userInfo.Login | 	user.PreferredUsername = userInfo.Login | ||||||
| 	user.Name = userInfo.Name | 	user.Name = userInfo.Name | ||||||
|  |  | ||||||
| 	// Return |  | ||||||
| 	return user, nil | 	return user, nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -22,49 +22,35 @@ func GoogleScopes() []string { | |||||||
| } | } | ||||||
|  |  | ||||||
| func GetGoogleUser(client *http.Client) (constants.Claims, error) { | func GetGoogleUser(client *http.Client) (constants.Claims, error) { | ||||||
| 	// Create user struct |  | ||||||
| 	var user constants.Claims | 	var user constants.Claims | ||||||
|  |  | ||||||
| 	// Get the user info from google using the oauth http client |  | ||||||
| 	res, err := client.Get("https://www.googleapis.com/userinfo/v2/me") | 	res, err := client.Get("https://www.googleapis.com/userinfo/v2/me") | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return user, err | 		return user, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	defer res.Body.Close() | 	defer res.Body.Close() | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Got response from google") | 	log.Debug().Msg("Got response from google") | ||||||
|  |  | ||||||
| 	// Read the body of the response |  | ||||||
| 	body, err := io.ReadAll(res.Body) | 	body, err := io.ReadAll(res.Body) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return user, err | 		return user, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Read body from google") | 	log.Debug().Msg("Read body from google") | ||||||
|  |  | ||||||
| 	// Create a new user info struct |  | ||||||
| 	var userInfo GoogleUserInfoResponse | 	var userInfo GoogleUserInfoResponse | ||||||
|  |  | ||||||
| 	// Unmarshal the body into the user struct |  | ||||||
| 	err = json.Unmarshal(body, &userInfo) | 	err = json.Unmarshal(body, &userInfo) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return user, err | 		return user, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Parsed user from google") | 	log.Debug().Msg("Parsed user from google") | ||||||
|  |  | ||||||
| 	// Map the user info to the user struct |  | ||||||
| 	user.PreferredUsername = strings.Split(userInfo.Email, "@")[0] | 	user.PreferredUsername = strings.Split(userInfo.Email, "@")[0] | ||||||
| 	user.Name = userInfo.Name | 	user.Name = userInfo.Name | ||||||
| 	user.Email = userInfo.Email | 	user.Email = userInfo.Email | ||||||
|  |  | ||||||
| 	// Return the user |  | ||||||
| 	return user, nil | 	return user, nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -11,12 +11,6 @@ import ( | |||||||
| 	"golang.org/x/oauth2/endpoints" | 	"golang.org/x/oauth2/endpoints" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func NewProviders(config types.OAuthConfig) *Providers { |  | ||||||
| 	return &Providers{ |  | ||||||
| 		Config: config, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type Providers struct { | type Providers struct { | ||||||
| 	Config  types.OAuthConfig | 	Config  types.OAuthConfig | ||||||
| 	Github  *oauth.OAuth | 	Github  *oauth.OAuth | ||||||
| @@ -24,64 +18,51 @@ type Providers struct { | |||||||
| 	Generic *oauth.OAuth | 	Generic *oauth.OAuth | ||||||
| } | } | ||||||
|  |  | ||||||
| func (providers *Providers) Init() { | func NewProviders(config types.OAuthConfig) *Providers { | ||||||
| 	// If we have a client id and secret for github, initialize the oauth provider | 	providers := &Providers{ | ||||||
| 	if providers.Config.GithubClientId != "" && providers.Config.GithubClientSecret != "" { | 		Config: config, | ||||||
| 		log.Info().Msg("Initializing Github OAuth") | 	} | ||||||
|  |  | ||||||
| 		// Create a new oauth provider with the github config | 	if config.GithubClientId != "" && config.GithubClientSecret != "" { | ||||||
|  | 		log.Info().Msg("Initializing Github OAuth") | ||||||
| 		providers.Github = oauth.NewOAuth(oauth2.Config{ | 		providers.Github = oauth.NewOAuth(oauth2.Config{ | ||||||
| 			ClientID:     providers.Config.GithubClientId, | 			ClientID:     config.GithubClientId, | ||||||
| 			ClientSecret: providers.Config.GithubClientSecret, | 			ClientSecret: config.GithubClientSecret, | ||||||
| 			RedirectURL:  fmt.Sprintf("%s/api/oauth/callback/github", providers.Config.AppURL), | 			RedirectURL:  fmt.Sprintf("%s/api/oauth/callback/github", config.AppURL), | ||||||
| 			Scopes:       GithubScopes(), | 			Scopes:       GithubScopes(), | ||||||
| 			Endpoint:     endpoints.GitHub, | 			Endpoint:     endpoints.GitHub, | ||||||
| 		}, false) | 		}, false) | ||||||
|  |  | ||||||
| 		// Initialize the oauth provider |  | ||||||
| 		providers.Github.Init() |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// If we have a client id and secret for google, initialize the oauth provider | 	if config.GoogleClientId != "" && config.GoogleClientSecret != "" { | ||||||
| 	if providers.Config.GoogleClientId != "" && providers.Config.GoogleClientSecret != "" { |  | ||||||
| 		log.Info().Msg("Initializing Google OAuth") | 		log.Info().Msg("Initializing Google OAuth") | ||||||
|  |  | ||||||
| 		// Create a new oauth provider with the google config |  | ||||||
| 		providers.Google = oauth.NewOAuth(oauth2.Config{ | 		providers.Google = oauth.NewOAuth(oauth2.Config{ | ||||||
| 			ClientID:     providers.Config.GoogleClientId, | 			ClientID:     config.GoogleClientId, | ||||||
| 			ClientSecret: providers.Config.GoogleClientSecret, | 			ClientSecret: config.GoogleClientSecret, | ||||||
| 			RedirectURL:  fmt.Sprintf("%s/api/oauth/callback/google", providers.Config.AppURL), | 			RedirectURL:  fmt.Sprintf("%s/api/oauth/callback/google", config.AppURL), | ||||||
| 			Scopes:       GoogleScopes(), | 			Scopes:       GoogleScopes(), | ||||||
| 			Endpoint:     endpoints.Google, | 			Endpoint:     endpoints.Google, | ||||||
| 		}, false) | 		}, false) | ||||||
|  |  | ||||||
| 		// Initialize the oauth provider |  | ||||||
| 		providers.Google.Init() |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// If we have a client id and secret for generic oauth, initialize the oauth provider | 	if config.GenericClientId != "" && config.GenericClientSecret != "" { | ||||||
| 	if providers.Config.GenericClientId != "" && providers.Config.GenericClientSecret != "" { |  | ||||||
| 		log.Info().Msg("Initializing Generic OAuth") | 		log.Info().Msg("Initializing Generic OAuth") | ||||||
|  |  | ||||||
| 		// Create a new oauth provider with the generic config |  | ||||||
| 		providers.Generic = oauth.NewOAuth(oauth2.Config{ | 		providers.Generic = oauth.NewOAuth(oauth2.Config{ | ||||||
| 			ClientID:     providers.Config.GenericClientId, | 			ClientID:     config.GenericClientId, | ||||||
| 			ClientSecret: providers.Config.GenericClientSecret, | 			ClientSecret: config.GenericClientSecret, | ||||||
| 			RedirectURL:  fmt.Sprintf("%s/api/oauth/callback/generic", providers.Config.AppURL), | 			RedirectURL:  fmt.Sprintf("%s/api/oauth/callback/generic", config.AppURL), | ||||||
| 			Scopes:       providers.Config.GenericScopes, | 			Scopes:       config.GenericScopes, | ||||||
| 			Endpoint: oauth2.Endpoint{ | 			Endpoint: oauth2.Endpoint{ | ||||||
| 				AuthURL:  providers.Config.GenericAuthURL, | 				AuthURL:  config.GenericAuthURL, | ||||||
| 				TokenURL: providers.Config.GenericTokenURL, | 				TokenURL: config.GenericTokenURL, | ||||||
| 			}, | 			}, | ||||||
| 		}, providers.Config.GenericSkipSSL) | 		}, config.GenericSkipSSL) | ||||||
|  |  | ||||||
| 		// Initialize the oauth provider |  | ||||||
| 		providers.Generic.Init() |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	return providers | ||||||
| } | } | ||||||
|  |  | ||||||
| func (providers *Providers) GetProvider(provider string) *oauth.OAuth { | func (providers *Providers) GetProvider(provider string) *oauth.OAuth { | ||||||
| 	// Return the provider based on the provider string |  | ||||||
| 	switch provider { | 	switch provider { | ||||||
| 	case "github": | 	case "github": | ||||||
| 		return providers.Github | 		return providers.Github | ||||||
| @@ -95,82 +76,63 @@ func (providers *Providers) GetProvider(provider string) *oauth.OAuth { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (providers *Providers) GetUser(provider string) (constants.Claims, error) { | func (providers *Providers) GetUser(provider string) (constants.Claims, error) { | ||||||
| 	// Create user struct |  | ||||||
| 	var user constants.Claims | 	var user constants.Claims | ||||||
|  |  | ||||||
| 	// Get the user from the provider | 	// Get the user from the provider | ||||||
| 	switch provider { | 	switch provider { | ||||||
| 	case "github": | 	case "github": | ||||||
| 		// If the github provider is not configured, return an error |  | ||||||
| 		if providers.Github == nil { | 		if providers.Github == nil { | ||||||
| 			log.Debug().Msg("Github provider not configured") | 			log.Debug().Msg("Github provider not configured") | ||||||
| 			return user, nil | 			return user, nil | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Get the client from the github provider |  | ||||||
| 		client := providers.Github.GetClient() | 		client := providers.Github.GetClient() | ||||||
|  |  | ||||||
| 		log.Debug().Msg("Got client from github") | 		log.Debug().Msg("Got client from github") | ||||||
|  |  | ||||||
| 		// Get the user from the github provider |  | ||||||
| 		user, err := GetGithubUser(client) | 		user, err := GetGithubUser(client) | ||||||
|  |  | ||||||
| 		// Check if there was an error |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return user, err | 			return user, err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		log.Debug().Msg("Got user from github") | 		log.Debug().Msg("Got user from github") | ||||||
|  |  | ||||||
| 		// Return the user |  | ||||||
| 		return user, nil | 		return user, nil | ||||||
| 	case "google": | 	case "google": | ||||||
| 		// If the google provider is not configured, return an error |  | ||||||
| 		if providers.Google == nil { | 		if providers.Google == nil { | ||||||
| 			log.Debug().Msg("Google provider not configured") | 			log.Debug().Msg("Google provider not configured") | ||||||
| 			return user, nil | 			return user, nil | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Get the client from the google provider |  | ||||||
| 		client := providers.Google.GetClient() | 		client := providers.Google.GetClient() | ||||||
|  |  | ||||||
| 		log.Debug().Msg("Got client from google") | 		log.Debug().Msg("Got client from google") | ||||||
|  |  | ||||||
| 		// Get the user from the google provider |  | ||||||
| 		user, err := GetGoogleUser(client) | 		user, err := GetGoogleUser(client) | ||||||
|  |  | ||||||
| 		// Check if there was an error |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return user, err | 			return user, err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		log.Debug().Msg("Got user from google") | 		log.Debug().Msg("Got user from google") | ||||||
|  |  | ||||||
| 		// Return the user |  | ||||||
| 		return user, nil | 		return user, nil | ||||||
| 	case "generic": | 	case "generic": | ||||||
| 		// If the generic provider is not configured, return an error |  | ||||||
| 		if providers.Generic == nil { | 		if providers.Generic == nil { | ||||||
| 			log.Debug().Msg("Generic provider not configured") | 			log.Debug().Msg("Generic provider not configured") | ||||||
| 			return user, nil | 			return user, nil | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Get the client from the generic provider |  | ||||||
| 		client := providers.Generic.GetClient() | 		client := providers.Generic.GetClient() | ||||||
|  |  | ||||||
| 		log.Debug().Msg("Got client from generic") | 		log.Debug().Msg("Got client from generic") | ||||||
|  |  | ||||||
| 		// Get the user from the generic provider |  | ||||||
| 		user, err := GetGenericUser(client, providers.Config.GenericUserURL) | 		user, err := GetGenericUser(client, providers.Config.GenericUserURL) | ||||||
|  |  | ||||||
| 		// Check if there was an error |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return user, err | 			return user, err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		log.Debug().Msg("Got user from generic") | 		log.Debug().Msg("Got user from generic") | ||||||
|  |  | ||||||
| 		// Return the email |  | ||||||
| 		return user, nil | 		return user, nil | ||||||
| 	default: | 	default: | ||||||
| 		return user, nil | 		return user, nil | ||||||
| @@ -178,7 +140,6 @@ func (providers *Providers) GetUser(provider string) (constants.Claims, error) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (provider *Providers) GetConfiguredProviders() []string { | func (provider *Providers) GetConfiguredProviders() []string { | ||||||
| 	// Create a list of the configured providers |  | ||||||
| 	providers := []string{} | 	providers := []string{} | ||||||
| 	if provider.Github != nil { | 	if provider.Github != nil { | ||||||
| 		providers = append(providers, "github") | 		providers = append(providers, "github") | ||||||
|   | |||||||
							
								
								
									
										130
									
								
								internal/server/server.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								internal/server/server.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,130 @@ | |||||||
|  | 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") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -21,6 +21,7 @@ type UnauthorizedQuery struct { | |||||||
| 	Username string `url:"username"` | 	Username string `url:"username"` | ||||||
| 	Resource string `url:"resource"` | 	Resource string `url:"resource"` | ||||||
| 	GroupErr bool   `url:"groupErr"` | 	GroupErr bool   `url:"groupErr"` | ||||||
|  | 	IP       string `url:"ip"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // Proxy is the uri parameters for the proxy endpoint | // Proxy is the uri parameters for the proxy endpoint | ||||||
|   | |||||||
| @@ -34,8 +34,14 @@ type Config struct { | |||||||
| 	EnvFile                 string `mapstructure:"env-file"` | 	EnvFile                 string `mapstructure:"env-file"` | ||||||
| 	LoginTimeout            int    `mapstructure:"login-timeout"` | 	LoginTimeout            int    `mapstructure:"login-timeout"` | ||||||
| 	LoginMaxRetries         int    `mapstructure:"login-max-retries"` | 	LoginMaxRetries         int    `mapstructure:"login-max-retries"` | ||||||
| 	FogotPasswordMessage    string `mapstructure:"forgot-password-message" validate:"required"` | 	FogotPasswordMessage    string `mapstructure:"forgot-password-message"` | ||||||
| 	BackgroundImage         string `mapstructure:"background-image" validate:"required"` | 	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"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // Server configuration | // Server configuration | ||||||
| @@ -69,8 +75,8 @@ type OAuthConfig struct { | |||||||
| 	AppURL              string | 	AppURL              string | ||||||
| } | } | ||||||
|  |  | ||||||
| // APIConfig is the configuration for the API | // ServerConfig is the configuration for the server | ||||||
| type APIConfig struct { | type ServerConfig struct { | ||||||
| 	Port    int | 	Port    int | ||||||
| 	Address string | 	Address string | ||||||
| } | } | ||||||
| @@ -80,15 +86,62 @@ type AuthConfig struct { | |||||||
| 	Users             Users | 	Users             Users | ||||||
| 	OauthWhitelist    string | 	OauthWhitelist    string | ||||||
| 	SessionExpiry     int | 	SessionExpiry     int | ||||||
| 	Secret            string |  | ||||||
| 	CookieSecure      bool | 	CookieSecure      bool | ||||||
| 	Domain            string | 	Domain            string | ||||||
| 	LoginTimeout      int | 	LoginTimeout      int | ||||||
| 	LoginMaxRetries   int | 	LoginMaxRetries   int | ||||||
| 	SessionCookieName string | 	SessionCookieName string | ||||||
|  | 	HMACSecret        string | ||||||
|  | 	EncryptionSecret  string | ||||||
| } | } | ||||||
|  |  | ||||||
| // HooksConfig is the configuration for the hooks service | // HooksConfig is the configuration for the hooks service | ||||||
| type HooksConfig struct { | type HooksConfig struct { | ||||||
| 	Domain string | 	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 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // PassowrdLabels is a struct that contains the password labels for a tinyauth protected container | ||||||
|  | type PassowrdLabels 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 | ||||||
|  | 	Headers []string | ||||||
|  | 	Domain  []string | ||||||
|  | 	Basic   BasicLabels | ||||||
|  | 	OAuth   OAuthLabels | ||||||
|  | 	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 | ||||||
|  | } | ||||||
|   | |||||||
| @@ -12,6 +12,12 @@ type User struct { | |||||||
| 	TotpSecret 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 | // Users is a list of users | ||||||
| type Users []User | type Users []User | ||||||
|  |  | ||||||
| @@ -32,15 +38,6 @@ type SessionCookie struct { | |||||||
| 	OAuthGroups string | 	OAuthGroups string | ||||||
| } | } | ||||||
|  |  | ||||||
| // TinyauthLabels is the labels for the tinyauth container |  | ||||||
| type TinyauthLabels struct { |  | ||||||
| 	OAuthWhitelist string |  | ||||||
| 	Users          string |  | ||||||
| 	Allowed        string |  | ||||||
| 	Headers        map[string]string |  | ||||||
| 	OAuthGroups    string |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // UserContext is the context for the user | // UserContext is the context for the user | ||||||
| type UserContext struct { | type UserContext struct { | ||||||
| 	Username    string | 	Username    string | ||||||
|   | |||||||
| @@ -1,15 +1,21 @@ | |||||||
| package utils | package utils | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"crypto/sha256" | ||||||
|  | 	"encoding/base64" | ||||||
| 	"errors" | 	"errors" | ||||||
|  | 	"io" | ||||||
|  | 	"net" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"os" | 	"os" | ||||||
| 	"regexp" | 	"regexp" | ||||||
| 	"slices" |  | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"tinyauth/internal/constants" |  | ||||||
| 	"tinyauth/internal/types" | 	"tinyauth/internal/types" | ||||||
|  |  | ||||||
|  | 	"github.com/traefik/paerser/parser" | ||||||
|  | 	"golang.org/x/crypto/hkdf" | ||||||
|  |  | ||||||
| 	"github.com/google/uuid" | 	"github.com/google/uuid" | ||||||
| 	"github.com/rs/zerolog/log" | 	"github.com/rs/zerolog/log" | ||||||
| ) | ) | ||||||
| @@ -18,201 +24,143 @@ import ( | |||||||
| func ParseUsers(users string) (types.Users, error) { | func ParseUsers(users string) (types.Users, error) { | ||||||
| 	log.Debug().Msg("Parsing users") | 	log.Debug().Msg("Parsing users") | ||||||
|  |  | ||||||
| 	// Create a new users struct |  | ||||||
| 	var usersParsed types.Users | 	var usersParsed types.Users | ||||||
|  |  | ||||||
| 	// Split the users by comma |  | ||||||
| 	userList := strings.Split(users, ",") | 	userList := strings.Split(users, ",") | ||||||
|  |  | ||||||
| 	// Check if there are any users |  | ||||||
| 	if len(userList) == 0 { | 	if len(userList) == 0 { | ||||||
| 		return types.Users{}, errors.New("invalid user format") | 		return types.Users{}, errors.New("invalid user format") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Loop through the users and split them by colon |  | ||||||
| 	for _, user := range userList { | 	for _, user := range userList { | ||||||
| 		parsed, err := ParseUser(user) | 		parsed, err := ParseUser(user) | ||||||
|  |  | ||||||
| 		// Check if there was an error |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return types.Users{}, err | 			return types.Users{}, err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Append the user to the users struct |  | ||||||
| 		usersParsed = append(usersParsed, parsed) | 		usersParsed = append(usersParsed, parsed) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Parsed users") | 	log.Debug().Msg("Parsed users") | ||||||
|  |  | ||||||
| 	// Return the users struct |  | ||||||
| 	return usersParsed, nil | 	return usersParsed, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // Get upper domain parses a hostname and returns the upper domain (e.g. sub1.sub2.domain.com -> sub2.domain.com) | // 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) { | func GetUpperDomain(urlSrc string) (string, error) { | ||||||
| 	// Make sure the url is valid |  | ||||||
| 	urlParsed, err := url.Parse(urlSrc) | 	urlParsed, err := url.Parse(urlSrc) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return "", err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Split the hostname by period |  | ||||||
| 	urlSplitted := strings.Split(urlParsed.Hostname(), ".") | 	urlSplitted := strings.Split(urlParsed.Hostname(), ".") | ||||||
|  |  | ||||||
| 	// Get the last part of the url |  | ||||||
| 	urlFinal := strings.Join(urlSplitted[1:], ".") | 	urlFinal := strings.Join(urlSplitted[1:], ".") | ||||||
|  |  | ||||||
| 	// Return the root domain |  | ||||||
| 	return urlFinal, nil | 	return urlFinal, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // Reads a file and returns the contents | // Reads a file and returns the contents | ||||||
| func ReadFile(file string) (string, error) { | func ReadFile(file string) (string, error) { | ||||||
| 	// Check if the file exists |  | ||||||
| 	_, err := os.Stat(file) | 	_, err := os.Stat(file) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return "", err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Read the file |  | ||||||
| 	data, err := os.ReadFile(file) | 	data, err := os.ReadFile(file) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return "", err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Return the file contents |  | ||||||
| 	return string(data), nil | 	return string(data), nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // Parses a file into a comma separated list of users | // Parses a file into a comma separated list of users | ||||||
| func ParseFileToLine(content string) string { | func ParseFileToLine(content string) string { | ||||||
| 	// Split the content by newline |  | ||||||
| 	lines := strings.Split(content, "\n") | 	lines := strings.Split(content, "\n") | ||||||
|  |  | ||||||
| 	// Create a list of users |  | ||||||
| 	users := make([]string, 0) | 	users := make([]string, 0) | ||||||
|  |  | ||||||
| 	// Loop through the lines, trimming the whitespace and appending to the users list |  | ||||||
| 	for _, line := range lines { | 	for _, line := range lines { | ||||||
| 		if strings.TrimSpace(line) == "" { | 		if strings.TrimSpace(line) == "" { | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		users = append(users, strings.TrimSpace(line)) | 		users = append(users, strings.TrimSpace(line)) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Return the users as a comma separated string |  | ||||||
| 	return strings.Join(users, ",") | 	return strings.Join(users, ",") | ||||||
| } | } | ||||||
|  |  | ||||||
| // Get the secret from the config or file | // Get the secret from the config or file | ||||||
| func GetSecret(conf string, file string) string { | func GetSecret(conf string, file string) string { | ||||||
| 	// If neither the config or file is set, return an empty string |  | ||||||
| 	if conf == "" && file == "" { | 	if conf == "" && file == "" { | ||||||
| 		return "" | 		return "" | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// If the config is set, return the config (environment variable) |  | ||||||
| 	if conf != "" { | 	if conf != "" { | ||||||
| 		return conf | 		return conf | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// If the file is set, read the file |  | ||||||
| 	contents, err := ReadFile(file) | 	contents, err := ReadFile(file) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "" | 		return "" | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Return the contents of the file |  | ||||||
| 	return ParseSecretFile(contents) | 	return ParseSecretFile(contents) | ||||||
| } | } | ||||||
|  |  | ||||||
| // Get the users from the config or file | // Get the users from the config or file | ||||||
| func GetUsers(conf string, file string) (types.Users, error) { | func GetUsers(conf string, file string) (types.Users, error) { | ||||||
| 	// Create a string to store the users |  | ||||||
| 	var users string | 	var users string | ||||||
|  |  | ||||||
| 	// If neither the config or file is set, return an empty users struct |  | ||||||
| 	if conf == "" && file == "" { | 	if conf == "" && file == "" { | ||||||
| 		return types.Users{}, nil | 		return types.Users{}, nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// If the config (environment) is set, append the users to the users string |  | ||||||
| 	if conf != "" { | 	if conf != "" { | ||||||
| 		log.Debug().Msg("Using users from config") | 		log.Debug().Msg("Using users from config") | ||||||
| 		users += conf | 		users += conf | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// If the file is set, read the file and append the users to the users string |  | ||||||
| 	if file != "" { | 	if file != "" { | ||||||
| 		// Read the file |  | ||||||
| 		contents, err := ReadFile(file) | 		contents, err := ReadFile(file) | ||||||
|  |  | ||||||
| 		// If there isn't an error we can append the users to the users string |  | ||||||
| 		if err == nil { | 		if err == nil { | ||||||
| 			log.Debug().Msg("Using users from file") | 			log.Debug().Msg("Using users from file") | ||||||
|  |  | ||||||
| 			// Append the users to the users string |  | ||||||
| 			if users != "" { | 			if users != "" { | ||||||
| 				users += "," | 				users += "," | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			// Parse the file contents into a comma separated list of users |  | ||||||
| 			users += ParseFileToLine(contents) | 			users += ParseFileToLine(contents) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Return the parsed users |  | ||||||
| 	return ParseUsers(users) | 	return ParseUsers(users) | ||||||
| } | } | ||||||
|  |  | ||||||
| // Parse the docker labels to the tinyauth labels struct | // Parse the headers in a map[string]string format | ||||||
| func GetTinyauthLabels(labels map[string]string) types.TinyauthLabels { | func ParseHeaders(headers []string) map[string]string { | ||||||
| 	// Create a new tinyauth labels struct | 	headerMap := make(map[string]string) | ||||||
| 	var tinyauthLabels types.TinyauthLabels |  | ||||||
|  |  | ||||||
| 	// Loop through the labels | 	for _, header := range headers { | ||||||
| 	for label, value := range labels { | 		split := strings.SplitN(header, "=", 2) | ||||||
|  | 		if len(split) != 2 || strings.TrimSpace(split[0]) == "" || strings.TrimSpace(split[1]) == "" { | ||||||
| 		// Check if the label is in the tinyauth labels | 			log.Warn().Str("header", header).Msg("Invalid header format, skipping") | ||||||
| 		if slices.Contains(constants.TinyauthLabels, label) { | 			continue | ||||||
|  |  | ||||||
| 			log.Debug().Str("label", label).Msg("Found label") |  | ||||||
|  |  | ||||||
| 			// Add the label value to the tinyauth labels struct |  | ||||||
| 			switch label { |  | ||||||
| 			case "tinyauth.oauth.whitelist": |  | ||||||
| 				tinyauthLabels.OAuthWhitelist = value |  | ||||||
| 			case "tinyauth.users": |  | ||||||
| 				tinyauthLabels.Users = value |  | ||||||
| 			case "tinyauth.allowed": |  | ||||||
| 				tinyauthLabels.Allowed = value |  | ||||||
| 			case "tinyauth.headers": |  | ||||||
| 				tinyauthLabels.Headers = make(map[string]string) |  | ||||||
| 				headers := strings.Split(value, ",") |  | ||||||
| 				for _, header := range headers { |  | ||||||
| 					headerSplit := strings.Split(header, "=") |  | ||||||
| 					if len(headerSplit) != 2 { |  | ||||||
| 						continue |  | ||||||
| 					} |  | ||||||
| 					tinyauthLabels.Headers[headerSplit[0]] = headerSplit[1] |  | ||||||
| 				} |  | ||||||
| 			case "tinyauth.oauth.groups": |  | ||||||
| 				tinyauthLabels.OAuthGroups = value |  | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
|  | 		key := SanitizeHeader(strings.TrimSpace(split[0])) | ||||||
|  | 		value := SanitizeHeader(strings.TrimSpace(split[1])) | ||||||
|  | 		headerMap[key] = value | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Return the tinyauth labels | 	return headerMap | ||||||
| 	return tinyauthLabels | } | ||||||
|  |  | ||||||
|  | // 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 | // Check if any of the OAuth providers are configured based on the client id and secret | ||||||
| @@ -232,27 +180,22 @@ func Filter[T any](slice []T, test func(T) bool) (res []T) { | |||||||
|  |  | ||||||
| // Parse user | // Parse user | ||||||
| func ParseUser(user string) (types.User, error) { | func ParseUser(user string) (types.User, error) { | ||||||
| 	// Check if the user is escaped |  | ||||||
| 	if strings.Contains(user, "$$") { | 	if strings.Contains(user, "$$") { | ||||||
| 		user = strings.ReplaceAll(user, "$$", "$") | 		user = strings.ReplaceAll(user, "$$", "$") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Split the user by colon |  | ||||||
| 	userSplit := strings.Split(user, ":") | 	userSplit := strings.Split(user, ":") | ||||||
|  |  | ||||||
| 	// Check if the user is in the correct format |  | ||||||
| 	if len(userSplit) < 2 || len(userSplit) > 3 { | 	if len(userSplit) < 2 || len(userSplit) > 3 { | ||||||
| 		return types.User{}, errors.New("invalid user format") | 		return types.User{}, errors.New("invalid user format") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Check for empty strings |  | ||||||
| 	for _, userPart := range userSplit { | 	for _, userPart := range userSplit { | ||||||
| 		if strings.TrimSpace(userPart) == "" { | 		if strings.TrimSpace(userPart) == "" { | ||||||
| 			return types.User{}, errors.New("invalid user format") | 			return types.User{}, errors.New("invalid user format") | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Check if the user has a totp secret |  | ||||||
| 	if len(userSplit) == 2 { | 	if len(userSplit) == 2 { | ||||||
| 		return types.User{ | 		return types.User{ | ||||||
| 			Username: strings.TrimSpace(userSplit[0]), | 			Username: strings.TrimSpace(userSplit[0]), | ||||||
| @@ -260,7 +203,6 @@ func ParseUser(user string) (types.User, error) { | |||||||
| 		}, nil | 		}, nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Return the user struct |  | ||||||
| 	return types.User{ | 	return types.User{ | ||||||
| 		Username:   strings.TrimSpace(userSplit[0]), | 		Username:   strings.TrimSpace(userSplit[0]), | ||||||
| 		Password:   strings.TrimSpace(userSplit[1]), | 		Password:   strings.TrimSpace(userSplit[1]), | ||||||
| @@ -270,60 +212,44 @@ func ParseUser(user string) (types.User, error) { | |||||||
|  |  | ||||||
| // Parse secret file | // Parse secret file | ||||||
| func ParseSecretFile(contents string) string { | func ParseSecretFile(contents string) string { | ||||||
| 	// Split to lines |  | ||||||
| 	lines := strings.Split(contents, "\n") | 	lines := strings.Split(contents, "\n") | ||||||
|  |  | ||||||
| 	// Loop through the lines |  | ||||||
| 	for _, line := range lines { | 	for _, line := range lines { | ||||||
| 		// Check if the line is empty |  | ||||||
| 		if strings.TrimSpace(line) == "" { | 		if strings.TrimSpace(line) == "" { | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Return the line |  | ||||||
| 		return strings.TrimSpace(line) | 		return strings.TrimSpace(line) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Return an empty string |  | ||||||
| 	return "" | 	return "" | ||||||
| } | } | ||||||
|  |  | ||||||
| // Check if a string matches a regex or a whitelist | // Check if a string matches a regex or if it is included in a comma separated list | ||||||
| func CheckWhitelist(whitelist string, str string) bool { | func CheckFilter(filter string, str string) bool { | ||||||
| 	// Check if the whitelist is empty | 	if len(strings.TrimSpace(filter)) == 0 { | ||||||
| 	if len(strings.TrimSpace(whitelist)) == 0 { |  | ||||||
| 		return true | 		return true | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Check if the whitelist is a regex | 	if strings.HasPrefix(filter, "/") && strings.HasSuffix(filter, "/") { | ||||||
| 	if strings.HasPrefix(whitelist, "/") && strings.HasSuffix(whitelist, "/") { | 		re, err := regexp.Compile(filter[1 : len(filter)-1]) | ||||||
| 		// Create regex |  | ||||||
| 		re, err := regexp.Compile(whitelist[1 : len(whitelist)-1]) |  | ||||||
|  |  | ||||||
| 		// Check if there was an error |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Error().Err(err).Msg("Error compiling regex") | 			log.Error().Err(err).Msg("Error compiling regex") | ||||||
| 			return false | 			return false | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Check if the string matches the regex |  | ||||||
| 		if re.MatchString(str) { | 		if re.MatchString(str) { | ||||||
| 			return true | 			return true | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Split the whitelist by comma | 	filterSplit := strings.Split(filter, ",") | ||||||
| 	whitelistSplit := strings.Split(whitelist, ",") |  | ||||||
|  |  | ||||||
| 	// Loop through the whitelist | 	for _, item := range filterSplit { | ||||||
| 	for _, item := range whitelistSplit { |  | ||||||
| 		// Check if the item matches with the string |  | ||||||
| 		if strings.TrimSpace(item) == str { | 		if strings.TrimSpace(item) == str { | ||||||
| 			return true | 			return true | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Return false if no match was found |  | ||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -348,15 +274,77 @@ func SanitizeHeader(header string) string { | |||||||
|  |  | ||||||
| // Generate a static identifier from a string | // Generate a static identifier from a string | ||||||
| func GenerateIdentifier(str string) string { | func GenerateIdentifier(str string) string { | ||||||
| 	// Create a new UUID |  | ||||||
| 	uuid := uuid.NewSHA1(uuid.NameSpaceURL, []byte(str)) | 	uuid := uuid.NewSHA1(uuid.NameSpaceURL, []byte(str)) | ||||||
|  |  | ||||||
| 	// Convert the UUID to a string |  | ||||||
| 	uuidString := uuid.String() | 	uuidString := uuid.String() | ||||||
|  |  | ||||||
| 	// Show the UUID |  | ||||||
| 	log.Debug().Str("uuid", uuidString).Msg("Generated UUID") | 	log.Debug().Str("uuid", uuidString).Msg("Generated UUID") | ||||||
|  |  | ||||||
| 	// Convert the UUID to a string |  | ||||||
| 	return strings.Split(uuidString, "-")[0] | 	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 "" | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
| @@ -9,11 +9,9 @@ import ( | |||||||
| 	"tinyauth/internal/utils" | 	"tinyauth/internal/utils" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Test the parse users function |  | ||||||
| func TestParseUsers(t *testing.T) { | func TestParseUsers(t *testing.T) { | ||||||
| 	t.Log("Testing parse users with a valid string") | 	t.Log("Testing parse users with a valid string") | ||||||
|  |  | ||||||
| 	// Test the parse users function with a valid string |  | ||||||
| 	users := "user1:pass1,user2:pass2" | 	users := "user1:pass1,user2:pass2" | ||||||
| 	expected := types.Users{ | 	expected := types.Users{ | ||||||
| 		{ | 		{ | ||||||
| @@ -27,154 +25,116 @@ func TestParseUsers(t *testing.T) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	result, err := utils.ParseUsers(users) | 	result, err := utils.ParseUsers(users) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error parsing users: %v", err) | 		t.Fatalf("Error parsing users: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Check if the result is equal to the expected |  | ||||||
| 	if !reflect.DeepEqual(expected, result) { | 	if !reflect.DeepEqual(expected, result) { | ||||||
| 		t.Fatalf("Expected %v, got %v", expected, result) | 		t.Fatalf("Expected %v, got %v", expected, result) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Test the get upper domain function |  | ||||||
| func TestGetUpperDomain(t *testing.T) { | func TestGetUpperDomain(t *testing.T) { | ||||||
| 	t.Log("Testing get upper domain with a valid url") | 	t.Log("Testing get upper domain with a valid url") | ||||||
|  |  | ||||||
| 	// Test the get upper domain function with a valid url |  | ||||||
| 	url := "https://sub1.sub2.domain.com:8080" | 	url := "https://sub1.sub2.domain.com:8080" | ||||||
| 	expected := "sub2.domain.com" | 	expected := "sub2.domain.com" | ||||||
|  |  | ||||||
| 	result, err := utils.GetUpperDomain(url) | 	result, err := utils.GetUpperDomain(url) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error getting root url: %v", err) | 		t.Fatalf("Error getting root url: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Check if the result is equal to the expected |  | ||||||
| 	if expected != result { | 	if expected != result { | ||||||
| 		t.Fatalf("Expected %v, got %v", expected, result) | 		t.Fatalf("Expected %v, got %v", expected, result) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Test the read file function |  | ||||||
| func TestReadFile(t *testing.T) { | func TestReadFile(t *testing.T) { | ||||||
| 	t.Log("Creating a test file") | 	t.Log("Creating a test file") | ||||||
|  |  | ||||||
| 	// Create a test file |  | ||||||
| 	err := os.WriteFile("/tmp/test.txt", []byte("test"), 0644) | 	err := os.WriteFile("/tmp/test.txt", []byte("test"), 0644) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error creating test file: %v", err) | 		t.Fatalf("Error creating test file: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Test the read file function |  | ||||||
| 	t.Log("Testing read file with a valid file") | 	t.Log("Testing read file with a valid file") | ||||||
|  |  | ||||||
| 	data, err := utils.ReadFile("/tmp/test.txt") | 	data, err := utils.ReadFile("/tmp/test.txt") | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error reading file: %v", err) | 		t.Fatalf("Error reading file: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Check if the data is equal to the expected |  | ||||||
| 	if data != "test" { | 	if data != "test" { | ||||||
| 		t.Fatalf("Expected test, got %v", data) | 		t.Fatalf("Expected test, got %v", data) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Cleanup the test file |  | ||||||
| 	t.Log("Cleaning up test file") | 	t.Log("Cleaning up test file") | ||||||
|  |  | ||||||
| 	err = os.Remove("/tmp/test.txt") | 	err = os.Remove("/tmp/test.txt") | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error cleaning up test file: %v", err) | 		t.Fatalf("Error cleaning up test file: %v", err) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Test the parse file to line function |  | ||||||
| func TestParseFileToLine(t *testing.T) { | func TestParseFileToLine(t *testing.T) { | ||||||
| 	t.Log("Testing parse file to line with a valid string") | 	t.Log("Testing parse file to line with a valid string") | ||||||
|  |  | ||||||
| 	// Test the parse file to line function with a valid string |  | ||||||
| 	content := "\nuser1:pass1\nuser2:pass2\n" | 	content := "\nuser1:pass1\nuser2:pass2\n" | ||||||
| 	expected := "user1:pass1,user2:pass2" | 	expected := "user1:pass1,user2:pass2" | ||||||
|  |  | ||||||
| 	result := utils.ParseFileToLine(content) | 	result := utils.ParseFileToLine(content) | ||||||
|  |  | ||||||
| 	// Check if the result is equal to the expected |  | ||||||
| 	if expected != result { | 	if expected != result { | ||||||
| 		t.Fatalf("Expected %v, got %v", expected, result) | 		t.Fatalf("Expected %v, got %v", expected, result) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Test the get secret function |  | ||||||
| func TestGetSecret(t *testing.T) { | func TestGetSecret(t *testing.T) { | ||||||
| 	t.Log("Testing get secret with an empty config and file") | 	t.Log("Testing get secret with an empty config and file") | ||||||
|  |  | ||||||
| 	// Test the get secret function with an empty config and file |  | ||||||
| 	conf := "" | 	conf := "" | ||||||
| 	file := "/tmp/test.txt" | 	file := "/tmp/test.txt" | ||||||
| 	expected := "test" | 	expected := "test" | ||||||
|  |  | ||||||
| 	// Create file |  | ||||||
| 	err := os.WriteFile(file, []byte(fmt.Sprintf("\n\n    \n\n\n  %s   \n\n    \n  ", expected)), 0644) | 	err := os.WriteFile(file, []byte(fmt.Sprintf("\n\n    \n\n\n  %s   \n\n    \n  ", expected)), 0644) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error creating test file: %v", err) | 		t.Fatalf("Error creating test file: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Test |  | ||||||
| 	result := utils.GetSecret(conf, file) | 	result := utils.GetSecret(conf, file) | ||||||
|  |  | ||||||
| 	// Check if the result is equal to the expected |  | ||||||
| 	if result != expected { | 	if result != expected { | ||||||
| 		t.Fatalf("Expected %v, got %v", expected, result) | 		t.Fatalf("Expected %v, got %v", expected, result) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	t.Log("Testing get secret with an empty file and a valid config") | 	t.Log("Testing get secret with an empty file and a valid config") | ||||||
|  |  | ||||||
| 	// Test the get secret function with an empty file and a valid config |  | ||||||
| 	result = utils.GetSecret(expected, "") | 	result = utils.GetSecret(expected, "") | ||||||
|  |  | ||||||
| 	// Check if the result is equal to the expected |  | ||||||
| 	if result != expected { | 	if result != expected { | ||||||
| 		t.Fatalf("Expected %v, got %v", expected, result) | 		t.Fatalf("Expected %v, got %v", expected, result) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	t.Log("Testing get secret with both a valid config and file") | 	t.Log("Testing get secret with both a valid config and file") | ||||||
|  |  | ||||||
| 	// Test the get secret function with both a valid config and file |  | ||||||
| 	result = utils.GetSecret(expected, file) | 	result = utils.GetSecret(expected, file) | ||||||
|  |  | ||||||
| 	// Check if the result is equal to the expected |  | ||||||
| 	if result != expected { | 	if result != expected { | ||||||
| 		t.Fatalf("Expected %v, got %v", expected, result) | 		t.Fatalf("Expected %v, got %v", expected, result) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Cleanup the test file |  | ||||||
| 	t.Log("Cleaning up test file") | 	t.Log("Cleaning up test file") | ||||||
|  |  | ||||||
| 	err = os.Remove(file) | 	err = os.Remove(file) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error cleaning up test file: %v", err) | 		t.Fatalf("Error cleaning up test file: %v", err) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Test the get users function |  | ||||||
| func TestGetUsers(t *testing.T) { | func TestGetUsers(t *testing.T) { | ||||||
| 	t.Log("Testing get users with a config and no file") | 	t.Log("Testing get users with a config and no file") | ||||||
|  |  | ||||||
| 	// Test the get users function with a config and no file |  | ||||||
| 	conf := "user1:pass1,user2:pass2" | 	conf := "user1:pass1,user2:pass2" | ||||||
| 	file := "" | 	file := "" | ||||||
| 	expected := types.Users{ | 	expected := types.Users{ | ||||||
| @@ -189,20 +149,16 @@ func TestGetUsers(t *testing.T) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	result, err := utils.GetUsers(conf, file) | 	result, err := utils.GetUsers(conf, file) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error getting users: %v", err) | 		t.Fatalf("Error getting users: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Check if the result is equal to the expected |  | ||||||
| 	if !reflect.DeepEqual(expected, result) { | 	if !reflect.DeepEqual(expected, result) { | ||||||
| 		t.Fatalf("Expected %v, got %v", expected, result) | 		t.Fatalf("Expected %v, got %v", expected, result) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	t.Log("Testing get users with a file and no config") | 	t.Log("Testing get users with a file and no config") | ||||||
|  |  | ||||||
| 	// Test the get users function with a file and no config |  | ||||||
| 	conf = "" | 	conf = "" | ||||||
| 	file = "/tmp/test.txt" | 	file = "/tmp/test.txt" | ||||||
| 	expected = types.Users{ | 	expected = types.Users{ | ||||||
| @@ -216,28 +172,20 @@ func TestGetUsers(t *testing.T) { | |||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Create file |  | ||||||
| 	err = os.WriteFile(file, []byte("user1:pass1\nuser2:pass2"), 0644) | 	err = os.WriteFile(file, []byte("user1:pass1\nuser2:pass2"), 0644) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error creating test file: %v", err) | 		t.Fatalf("Error creating test file: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Test |  | ||||||
| 	result, err = utils.GetUsers(conf, file) | 	result, err = utils.GetUsers(conf, file) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error getting users: %v", err) | 		t.Fatalf("Error getting users: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Check if the result is equal to the expected |  | ||||||
| 	if !reflect.DeepEqual(expected, result) { | 	if !reflect.DeepEqual(expected, result) { | ||||||
| 		t.Fatalf("Expected %v, got %v", expected, result) | 		t.Fatalf("Expected %v, got %v", expected, result) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Test the get users function with both a config and file |  | ||||||
| 	t.Log("Testing get users with both a config and file") | 	t.Log("Testing get users with both a config and file") | ||||||
|  |  | ||||||
| 	conf = "user3:pass3" | 	conf = "user3:pass3" | ||||||
| @@ -257,82 +205,56 @@ func TestGetUsers(t *testing.T) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	result, err = utils.GetUsers(conf, file) | 	result, err = utils.GetUsers(conf, file) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error getting users: %v", err) | 		t.Fatalf("Error getting users: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Check if the result is equal to the expected |  | ||||||
| 	if !reflect.DeepEqual(expected, result) { | 	if !reflect.DeepEqual(expected, result) { | ||||||
| 		t.Fatalf("Expected %v, got %v", expected, result) | 		t.Fatalf("Expected %v, got %v", expected, result) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Cleanup the test file |  | ||||||
| 	t.Log("Cleaning up test file") | 	t.Log("Cleaning up test file") | ||||||
|  |  | ||||||
| 	err = os.Remove(file) | 	err = os.Remove(file) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error cleaning up test file: %v", err) | 		t.Fatalf("Error cleaning up test file: %v", err) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Test the tinyauth labels function | func TestGetLabels(t *testing.T) { | ||||||
| func TestGetTinyauthLabels(t *testing.T) { | 	t.Log("Testing get labels with a valid map") | ||||||
| 	t.Log("Testing get tinyauth labels with a valid map") |  | ||||||
|  |  | ||||||
| 	// Test the get tinyauth labels function with a valid map |  | ||||||
| 	labels := map[string]string{ | 	labels := map[string]string{ | ||||||
| 		"tinyauth.users":           "user1,user2", | 		"tinyauth.users":           "user1,user2", | ||||||
| 		"tinyauth.oauth.whitelist": "/regex/", | 		"tinyauth.oauth.whitelist": "/regex/", | ||||||
| 		"tinyauth.allowed":         "random", | 		"tinyauth.allowed":         "random", | ||||||
| 		"random":                   "random", |  | ||||||
| 		"tinyauth.headers":         "X-Header=value", | 		"tinyauth.headers":         "X-Header=value", | ||||||
|  | 		"tinyauth.oauth.groups":    "group1,group2", | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	expected := types.TinyauthLabels{ | 	expected := types.Labels{ | ||||||
| 		Users:          "user1,user2", | 		Users:   "user1,user2", | ||||||
| 		OAuthWhitelist: "/regex/", | 		Allowed: "random", | ||||||
| 		Allowed:        "random", | 		Headers: []string{"X-Header=value"}, | ||||||
| 		Headers: map[string]string{ | 		OAuth: types.OAuthLabels{ | ||||||
| 			"X-Header": "value", | 			Whitelist: "/regex/", | ||||||
|  | 			Groups:    "group1,group2", | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	result := utils.GetTinyauthLabels(labels) | 	result, err := utils.GetLabels(labels) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Error getting labels: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Check if the result is equal to the expected |  | ||||||
| 	if !reflect.DeepEqual(expected, result) { | 	if !reflect.DeepEqual(expected, result) { | ||||||
| 		t.Fatalf("Expected %v, got %v", expected, result) | 		t.Fatalf("Expected %v, got %v", expected, result) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Test the filter function |  | ||||||
| func TestFilter(t *testing.T) { |  | ||||||
| 	t.Log("Testing filter helper") |  | ||||||
|  |  | ||||||
| 	// Create variables |  | ||||||
| 	data := []string{"", "val1", "", "val2", "", "val3", ""} |  | ||||||
| 	expected := []string{"val1", "val2", "val3"} |  | ||||||
|  |  | ||||||
| 	// Test the filter function |  | ||||||
| 	result := utils.Filter(data, func(val string) bool { |  | ||||||
| 		return val != "" |  | ||||||
| 	}) |  | ||||||
|  |  | ||||||
| 	// Check if the result is equal to the expected |  | ||||||
| 	if !reflect.DeepEqual(expected, result) { |  | ||||||
| 		t.Fatalf("Expected %v, got %v", expected, result) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Test parse user |  | ||||||
| func TestParseUser(t *testing.T) { | func TestParseUser(t *testing.T) { | ||||||
| 	t.Log("Testing parse user with a valid user") | 	t.Log("Testing parse user with a valid user") | ||||||
|  |  | ||||||
| 	// Create variables |  | ||||||
| 	user := "user:pass:secret" | 	user := "user:pass:secret" | ||||||
| 	expected := types.User{ | 	expected := types.User{ | ||||||
| 		Username:   "user", | 		Username:   "user", | ||||||
| @@ -340,22 +262,17 @@ func TestParseUser(t *testing.T) { | |||||||
| 		TotpSecret: "secret", | 		TotpSecret: "secret", | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Test the parse user function |  | ||||||
| 	result, err := utils.ParseUser(user) | 	result, err := utils.ParseUser(user) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error parsing user: %v", err) | 		t.Fatalf("Error parsing user: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Check if the result is equal to the expected |  | ||||||
| 	if !reflect.DeepEqual(expected, result) { | 	if !reflect.DeepEqual(expected, result) { | ||||||
| 		t.Fatalf("Expected %v, got %v", expected, result) | 		t.Fatalf("Expected %v, got %v", expected, result) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	t.Log("Testing parse user with an escaped user") | 	t.Log("Testing parse user with an escaped user") | ||||||
|  |  | ||||||
| 	// Create variables |  | ||||||
| 	user = "user:p$$ass$$:secret" | 	user = "user:p$$ass$$:secret" | ||||||
| 	expected = types.User{ | 	expected = types.User{ | ||||||
| 		Username:   "user", | 		Username:   "user", | ||||||
| @@ -363,168 +280,268 @@ func TestParseUser(t *testing.T) { | |||||||
| 		TotpSecret: "secret", | 		TotpSecret: "secret", | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Test the parse user function |  | ||||||
| 	result, err = utils.ParseUser(user) | 	result, err = utils.ParseUser(user) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error parsing user: %v", err) | 		t.Fatalf("Error parsing user: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Check if the result is equal to the expected |  | ||||||
| 	if !reflect.DeepEqual(expected, result) { | 	if !reflect.DeepEqual(expected, result) { | ||||||
| 		t.Fatalf("Expected %v, got %v", expected, result) | 		t.Fatalf("Expected %v, got %v", expected, result) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	t.Log("Testing parse user with an invalid user") | 	t.Log("Testing parse user with an invalid user") | ||||||
|  |  | ||||||
| 	// Create variables |  | ||||||
| 	user = "user::pass" | 	user = "user::pass" | ||||||
|  |  | ||||||
| 	// Test the parse user function |  | ||||||
| 	_, err = utils.ParseUser(user) | 	_, err = utils.ParseUser(user) | ||||||
|  |  | ||||||
| 	// Check if there was an error |  | ||||||
| 	if err == nil { | 	if err == nil { | ||||||
| 		t.Fatalf("Expected error parsing user") | 		t.Fatalf("Expected error parsing user") | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Test the whitelist function | func TestCheckFilter(t *testing.T) { | ||||||
| func TestCheckWhitelist(t *testing.T) { | 	t.Log("Testing check filter with a comma separated list") | ||||||
| 	t.Log("Testing check whitelist with a comma whitelist") |  | ||||||
|  |  | ||||||
| 	// Create variables | 	filter := "user1,user2,user3" | ||||||
| 	whitelist := "user1,user2,user3" |  | ||||||
| 	str := "user1" | 	str := "user1" | ||||||
| 	expected := true | 	expected := true | ||||||
|  |  | ||||||
| 	// Test the check whitelist function | 	result := utils.CheckFilter(filter, str) | ||||||
| 	result := utils.CheckWhitelist(whitelist, str) |  | ||||||
|  |  | ||||||
| 	// Check if the result is equal to the expected |  | ||||||
| 	if result != expected { | 	if result != expected { | ||||||
| 		t.Fatalf("Expected %v, got %v", expected, result) | 		t.Fatalf("Expected %v, got %v", expected, result) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	t.Log("Testing check whitelist with a regex whitelist") | 	t.Log("Testing check filter with a regex filter") | ||||||
|  |  | ||||||
| 	// Create variables | 	filter = "/^user[0-9]+$/" | ||||||
| 	whitelist = "/^user[0-9]+$/" |  | ||||||
| 	str = "user1" | 	str = "user1" | ||||||
| 	expected = true | 	expected = true | ||||||
|  |  | ||||||
| 	// Test the check whitelist function | 	result = utils.CheckFilter(filter, str) | ||||||
| 	result = utils.CheckWhitelist(whitelist, str) |  | ||||||
|  |  | ||||||
| 	// Check if the result is equal to the expected |  | ||||||
| 	if result != expected { | 	if result != expected { | ||||||
| 		t.Fatalf("Expected %v, got %v", expected, result) | 		t.Fatalf("Expected %v, got %v", expected, result) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	t.Log("Testing check whitelist with an empty whitelist") | 	t.Log("Testing check filter with an empty filter") | ||||||
|  |  | ||||||
| 	// Create variables | 	filter = "" | ||||||
| 	whitelist = "" |  | ||||||
| 	str = "user1" | 	str = "user1" | ||||||
| 	expected = true | 	expected = true | ||||||
|  |  | ||||||
| 	// Test the check whitelist function | 	result = utils.CheckFilter(filter, str) | ||||||
| 	result = utils.CheckWhitelist(whitelist, str) |  | ||||||
|  |  | ||||||
| 	// Check if the result is equal to the expected |  | ||||||
| 	if result != expected { | 	if result != expected { | ||||||
| 		t.Fatalf("Expected %v, got %v", expected, result) | 		t.Fatalf("Expected %v, got %v", expected, result) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	t.Log("Testing check whitelist with an invalid regex whitelist") | 	t.Log("Testing check filter with an invalid regex filter") | ||||||
|  |  | ||||||
| 	// Create variables | 	filter = "/^user[0-9+$/" | ||||||
| 	whitelist = "/^user[0-9+$/" |  | ||||||
| 	str = "user1" | 	str = "user1" | ||||||
| 	expected = false | 	expected = false | ||||||
|  |  | ||||||
| 	// Test the check whitelist function | 	result = utils.CheckFilter(filter, str) | ||||||
| 	result = utils.CheckWhitelist(whitelist, str) |  | ||||||
|  |  | ||||||
| 	// Check if the result is equal to the expected |  | ||||||
| 	if result != expected { | 	if result != expected { | ||||||
| 		t.Fatalf("Expected %v, got %v", expected, result) | 		t.Fatalf("Expected %v, got %v", expected, result) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	t.Log("Testing check whitelist with a non matching whitelist") | 	t.Log("Testing check filter with a non matching list") | ||||||
|  |  | ||||||
| 	// Create variables | 	filter = "user1,user2,user3" | ||||||
| 	whitelist = "user1,user2,user3" |  | ||||||
| 	str = "user4" | 	str = "user4" | ||||||
| 	expected = false | 	expected = false | ||||||
|  |  | ||||||
| 	// Test the check whitelist function | 	result = utils.CheckFilter(filter, str) | ||||||
| 	result = utils.CheckWhitelist(whitelist, str) |  | ||||||
|  |  | ||||||
| 	// Check if the result is equal to the expected |  | ||||||
| 	if result != expected { | 	if result != expected { | ||||||
| 		t.Fatalf("Expected %v, got %v", expected, result) | 		t.Fatalf("Expected %v, got %v", expected, result) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Test capitalize |  | ||||||
| func TestCapitalize(t *testing.T) { |  | ||||||
| 	t.Log("Testing capitalize with a valid string") |  | ||||||
|  |  | ||||||
| 	// Create variables |  | ||||||
| 	str := "test" |  | ||||||
| 	expected := "Test" |  | ||||||
|  |  | ||||||
| 	// Test the capitalize function |  | ||||||
| 	result := utils.Capitalize(str) |  | ||||||
|  |  | ||||||
| 	// Check if the result is equal to the expected |  | ||||||
| 	if result != expected { |  | ||||||
| 		t.Fatalf("Expected %v, got %v", expected, result) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	t.Log("Testing capitalize with an empty string") |  | ||||||
|  |  | ||||||
| 	// Create variables |  | ||||||
| 	str = "" |  | ||||||
| 	expected = "" |  | ||||||
|  |  | ||||||
| 	// Test the capitalize function |  | ||||||
| 	result = utils.Capitalize(str) |  | ||||||
|  |  | ||||||
| 	// Check if the result is equal to the expected |  | ||||||
| 	if result != expected { |  | ||||||
| 		t.Fatalf("Expected %v, got %v", expected, result) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Test the header sanitizer |  | ||||||
| func TestSanitizeHeader(t *testing.T) { | func TestSanitizeHeader(t *testing.T) { | ||||||
| 	t.Log("Testing sanitize header with a valid string") | 	t.Log("Testing sanitize header with a valid string") | ||||||
|  |  | ||||||
| 	// Create variables |  | ||||||
| 	str := "X-Header=value" | 	str := "X-Header=value" | ||||||
| 	expected := "X-Header=value" | 	expected := "X-Header=value" | ||||||
|  |  | ||||||
| 	// Test the sanitize header function |  | ||||||
| 	result := utils.SanitizeHeader(str) | 	result := utils.SanitizeHeader(str) | ||||||
|  |  | ||||||
| 	// Check if the result is equal to the expected |  | ||||||
| 	if result != expected { | 	if result != expected { | ||||||
| 		t.Fatalf("Expected %v, got %v", expected, result) | 		t.Fatalf("Expected %v, got %v", expected, result) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	t.Log("Testing sanitize header with an invalid string") | 	t.Log("Testing sanitize header with an invalid string") | ||||||
|  |  | ||||||
| 	// Create variables |  | ||||||
| 	str = "X-Header=val\nue" | 	str = "X-Header=val\nue" | ||||||
| 	expected = "X-Header=value" | 	expected = "X-Header=value" | ||||||
|  |  | ||||||
| 	// Test the sanitize header function |  | ||||||
| 	result = utils.SanitizeHeader(str) | 	result = utils.SanitizeHeader(str) | ||||||
|  |  | ||||||
| 	// Check if the result is equal to the expected | 	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 { | 	if result != expected { | ||||||
| 		t.Fatalf("Expected %v, got %v", expected, result) | 		t.Fatalf("Expected %v, got %v", expected, result) | ||||||
| 	} | 	} | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								main.go
									
									
									
									
									
								
							| @@ -10,9 +10,6 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| func main() { | func main() { | ||||||
| 	// Logger |  | ||||||
| 	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().Logger().Level(zerolog.FatalLevel) | ||||||
|  |  | ||||||
| 	// Run cmd |  | ||||||
| 	cmd.Execute() | 	cmd.Execute() | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user