mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-11-03 23:55:44 +00:00 
			
		
		
		
	Compare commits
	
		
			19 Commits
		
	
	
		
			v3.3.0-alp
			...
			v3.3.1-bet
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					2db7795eb7 | ||
| 
						 | 
					dd5a9e2216 | ||
| 
						 | 
					00d1543f08 | ||
| 
						 | 
					d1eeb8c7f7 | ||
| 
						 | 
					a98a91a394 | ||
| 
						 | 
					5278fbea68 | ||
| 
						 | 
					773942dc3b | ||
| 
						 | 
					83483d6374 | ||
| 
						 | 
					aab01b3195 | ||
| 
						 | 
					fe5e07139f | ||
| 
						 | 
					93a75324b8 | ||
| 
						 | 
					67a01c196f | ||
| 
						 | 
					483b1de701 | ||
| 
						 | 
					40ceed6686 | ||
| 
						 | 
					3878c629c6 | ||
| 
						 | 
					31e874a34f | ||
| 
						 | 
					74a346349a | ||
| 
						 | 
					a9e8bf89a9 | ||
| 
						 | 
					f824b84787 | 
@@ -28,3 +28,4 @@ LOGIN_MAX_RETRIES=5
 | 
			
		||||
LOG_LEVEL=0
 | 
			
		||||
APP_TITLE=Tinyauth SSO
 | 
			
		||||
FORGOT_PASSWORD_MESSAGE=Some message about resetting the password
 | 
			
		||||
OAUTH_AUTO_REDIRECT=none
 | 
			
		||||
							
								
								
									
										31
									
								
								.github/workflows/sponsors.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								.github/workflows/sponsors.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
name: Generate Sponsors List
 | 
			
		||||
on:
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  generate-sponsors:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
 | 
			
		||||
      - name: Generate Sponsors
 | 
			
		||||
        uses: JamesIves/github-sponsors-readme-action@v1
 | 
			
		||||
        with:
 | 
			
		||||
          token: ${{ secrets.SPONSORS_GENERATOR_PAT }}
 | 
			
		||||
          active-only: false
 | 
			
		||||
          file: "README.md"
 | 
			
		||||
          template: '<a href="https://github.com/{{{ login }}}"><img src="{{{ avatarUrl }}}" width="64px" alt="User avatar: {{{ login }}}" /></a>  '
 | 
			
		||||
 | 
			
		||||
      - name: Create Pull Request
 | 
			
		||||
        uses: peter-evans/create-pull-request@v7
 | 
			
		||||
        with:
 | 
			
		||||
          token: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
          commit-message: |
 | 
			
		||||
            docs: regenerate readme sponsors list
 | 
			
		||||
          committer: GitHub <noreply@github.com>
 | 
			
		||||
          author: GitHub <noreply@github.com>
 | 
			
		||||
          branch: docs/update-readme
 | 
			
		||||
          title: |
 | 
			
		||||
            docs: regenerate readme sponsors list
 | 
			
		||||
          labels: bot
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
# Site builder
 | 
			
		||||
FROM oven/bun:1.2.10-alpine AS frontend-builder
 | 
			
		||||
FROM oven/bun:1.2.11-alpine AS frontend-builder
 | 
			
		||||
 | 
			
		||||
WORKDIR /frontend
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<div align="center">
 | 
			
		||||
    <img alt="Tinyauth" title="Tinyauth" width="256" src="frontend/public/logo.png">
 | 
			
		||||
    <img alt="Tinyauth" title="Tinyauth" height="256" src="frontend/public/logo.png">
 | 
			
		||||
    <h1>Tinyauth</h1>
 | 
			
		||||
    <p>The easiest way to secure your apps with a login screen.</p>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -53,9 +53,7 @@ Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You ma
 | 
			
		||||
 | 
			
		||||
Thanks a lot to the following people for providing me with more coffee:
 | 
			
		||||
 | 
			
		||||
| <img height="64" src="https://avatars.githubusercontent.com/u/47644445?v=4" alt="Nicolas"> | <img height="64" src="https://avatars.githubusercontent.com/u/4255748?v=4" alt="Erwin"> | <img height="64" src="https://avatars.githubusercontent.com/u/7935041?v=4" alt="SimpleHomelab" /> |
 | 
			
		||||
| ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
 | 
			
		||||
| <div align="center"><a href="https://github.com/nicotsx">Nicolas</a></div>                 | <div align="center"><a href="https://github.com/erwinkramer">Erwin</a></div>            | <div align="center"><a href="https://github.com/SimpleHomelab">SimpleHomelab</a></div>            |
 | 
			
		||||
<!-- 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>  <!-- sponsors -->
 | 
			
		||||
 | 
			
		||||
## Acknowledgements
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								cmd/root.go
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								cmd/root.go
									
									
									
									
									
								
							@@ -91,6 +91,7 @@ var rootCmd = &cobra.Command{
 | 
			
		||||
			CookieSecure:          config.CookieSecure,
 | 
			
		||||
			Domain:                domain,
 | 
			
		||||
			ForgotPasswordMessage: config.FogotPasswordMessage,
 | 
			
		||||
			OAuthAutoRedirect:     config.OAuthAutoRedirect,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Create api config
 | 
			
		||||
@@ -111,6 +112,11 @@ var rootCmd = &cobra.Command{
 | 
			
		||||
			LoginMaxRetries: config.LoginMaxRetries,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Create hooks config
 | 
			
		||||
		hooksConfig := types.HooksConfig{
 | 
			
		||||
			Domain: domain,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Create docker service
 | 
			
		||||
		docker := docker.NewDocker()
 | 
			
		||||
 | 
			
		||||
@@ -128,7 +134,7 @@ var rootCmd = &cobra.Command{
 | 
			
		||||
		providers.Init()
 | 
			
		||||
 | 
			
		||||
		// Create hooks service
 | 
			
		||||
		hooks := hooks.NewHooks(auth, providers)
 | 
			
		||||
		hooks := hooks.NewHooks(hooksConfig, auth, providers)
 | 
			
		||||
 | 
			
		||||
		// Create handlers
 | 
			
		||||
		handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker)
 | 
			
		||||
@@ -189,9 +195,10 @@ func init() {
 | 
			
		||||
	rootCmd.Flags().String("generic-auth-url", "", "Generic OAuth auth URL.")
 | 
			
		||||
	rootCmd.Flags().String("generic-token-url", "", "Generic OAuth token URL.")
 | 
			
		||||
	rootCmd.Flags().String("generic-user-url", "", "Generic OAuth user info URL.")
 | 
			
		||||
	rootCmd.Flags().String("generic-name", "Other", "Generic OAuth provider name.")
 | 
			
		||||
	rootCmd.Flags().String("generic-name", "Generic", "Generic OAuth provider name.")
 | 
			
		||||
	rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.")
 | 
			
		||||
	rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.")
 | 
			
		||||
	rootCmd.Flags().String("oauth-auto-redirect", "none", "Auto redirect to the specified OAuth provider if configured. (available providers: github, google, generic)")
 | 
			
		||||
	rootCmd.Flags().Int("session-expiry", 86400, "Session (cookie) expiration time in seconds.")
 | 
			
		||||
	rootCmd.Flags().Int("login-timeout", 300, "Login timeout in seconds after max retries reached (0 to disable).")
 | 
			
		||||
	rootCmd.Flags().Int("login-max-retries", 5, "Maximum login attempts before timeout (0 to disable).")
 | 
			
		||||
@@ -224,6 +231,7 @@ func init() {
 | 
			
		||||
	viper.BindEnv("generic-name", "GENERIC_NAME")
 | 
			
		||||
	viper.BindEnv("disable-continue", "DISABLE_CONTINUE")
 | 
			
		||||
	viper.BindEnv("oauth-whitelist", "OAUTH_WHITELIST")
 | 
			
		||||
	viper.BindEnv("oauth-auto-redirect", "OAUTH_AUTO_REDIRECT")
 | 
			
		||||
	viper.BindEnv("session-expiry", "SESSION_EXPIRY")
 | 
			
		||||
	viper.BindEnv("log-level", "LOG_LEVEL")
 | 
			
		||||
	viper.BindEnv("app-title", "APP_TITLE")
 | 
			
		||||
 
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										882
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										882
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -41,7 +41,7 @@
 | 
			
		||||
        "prettier": "3.5.3",
 | 
			
		||||
        "typescript": "~5.8.3",
 | 
			
		||||
        "typescript-eslint": "^8.18.2",
 | 
			
		||||
        "vite": "^6.0.5"
 | 
			
		||||
        "vite": "^6.3.4"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@babel/runtime": {
 | 
			
		||||
@@ -56,8 +56,282 @@
 | 
			
		||||
        "node": ">=6.9.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@esbuild/aix-ppc64": {
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "ppc64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "aix"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@esbuild/android-arm": {
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "android"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@esbuild/android-arm64": {
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "android"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@esbuild/android-x64": {
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "x64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "android"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@esbuild/darwin-arm64": {
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "darwin"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@esbuild/darwin-x64": {
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "x64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "darwin"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@esbuild/freebsd-arm64": {
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "freebsd"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@esbuild/freebsd-x64": {
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "x64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "freebsd"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@esbuild/linux-arm": {
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@esbuild/linux-arm64": {
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@esbuild/linux-ia32": {
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "ia32"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@esbuild/linux-loong64": {
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "loong64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@esbuild/linux-mips64el": {
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "mips64el"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@esbuild/linux-ppc64": {
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "ppc64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@esbuild/linux-riscv64": {
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "riscv64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@esbuild/linux-s390x": {
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "s390x"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@esbuild/linux-x64": {
 | 
			
		||||
      "version": "0.24.2",
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "x64"
 | 
			
		||||
      ],
 | 
			
		||||
@@ -71,6 +345,142 @@
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@esbuild/netbsd-arm64": {
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "netbsd"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@esbuild/netbsd-x64": {
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "x64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "netbsd"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@esbuild/openbsd-arm64": {
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "openbsd"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@esbuild/openbsd-x64": {
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "x64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "openbsd"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@esbuild/sunos-x64": {
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "x64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "sunos"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@esbuild/win32-arm64": {
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "win32"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@esbuild/win32-ia32": {
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "ia32"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "win32"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@esbuild/win32-x64": {
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "x64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "win32"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@eslint-community/eslint-utils": {
 | 
			
		||||
      "version": "4.4.1",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
@@ -399,8 +809,220 @@
 | 
			
		||||
        "node": ">= 8"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-android-arm-eabi": {
 | 
			
		||||
      "version": "4.40.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz",
 | 
			
		||||
      "integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "android"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-android-arm64": {
 | 
			
		||||
      "version": "4.40.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz",
 | 
			
		||||
      "integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "android"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-darwin-arm64": {
 | 
			
		||||
      "version": "4.40.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz",
 | 
			
		||||
      "integrity": "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "darwin"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-darwin-x64": {
 | 
			
		||||
      "version": "4.40.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz",
 | 
			
		||||
      "integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "x64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "darwin"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-freebsd-arm64": {
 | 
			
		||||
      "version": "4.40.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz",
 | 
			
		||||
      "integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "freebsd"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-freebsd-x64": {
 | 
			
		||||
      "version": "4.40.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz",
 | 
			
		||||
      "integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "x64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "freebsd"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
 | 
			
		||||
      "version": "4.40.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz",
 | 
			
		||||
      "integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
 | 
			
		||||
      "version": "4.40.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz",
 | 
			
		||||
      "integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-arm64-gnu": {
 | 
			
		||||
      "version": "4.40.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz",
 | 
			
		||||
      "integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-arm64-musl": {
 | 
			
		||||
      "version": "4.40.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz",
 | 
			
		||||
      "integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
 | 
			
		||||
      "version": "4.40.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz",
 | 
			
		||||
      "integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "loong64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
 | 
			
		||||
      "version": "4.40.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz",
 | 
			
		||||
      "integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "ppc64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
 | 
			
		||||
      "version": "4.40.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz",
 | 
			
		||||
      "integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "riscv64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-riscv64-musl": {
 | 
			
		||||
      "version": "4.40.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz",
 | 
			
		||||
      "integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "riscv64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-s390x-gnu": {
 | 
			
		||||
      "version": "4.40.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz",
 | 
			
		||||
      "integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "s390x"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-x64-gnu": {
 | 
			
		||||
      "version": "4.30.1",
 | 
			
		||||
      "version": "4.40.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz",
 | 
			
		||||
      "integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "x64"
 | 
			
		||||
      ],
 | 
			
		||||
@@ -412,7 +1034,9 @@
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-x64-musl": {
 | 
			
		||||
      "version": "4.30.1",
 | 
			
		||||
      "version": "4.40.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz",
 | 
			
		||||
      "integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "x64"
 | 
			
		||||
      ],
 | 
			
		||||
@@ -423,6 +1047,48 @@
 | 
			
		||||
        "linux"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-win32-arm64-msvc": {
 | 
			
		||||
      "version": "4.40.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz",
 | 
			
		||||
      "integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "win32"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-win32-ia32-msvc": {
 | 
			
		||||
      "version": "4.40.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz",
 | 
			
		||||
      "integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "ia32"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "win32"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-win32-x64-msvc": {
 | 
			
		||||
      "version": "4.40.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz",
 | 
			
		||||
      "integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "x64"
 | 
			
		||||
      ],
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "win32"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@swc/core": {
 | 
			
		||||
      "version": "1.10.7",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
@@ -539,7 +1205,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/estree": {
 | 
			
		||||
      "version": "1.0.6",
 | 
			
		||||
      "version": "1.0.7",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
 | 
			
		||||
      "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/estree-jsx": {
 | 
			
		||||
@@ -1099,7 +1767,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/esbuild": {
 | 
			
		||||
      "version": "0.24.2",
 | 
			
		||||
      "version": "0.25.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz",
 | 
			
		||||
      "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "hasInstallScript": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
@@ -1110,31 +1780,31 @@
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      },
 | 
			
		||||
      "optionalDependencies": {
 | 
			
		||||
        "@esbuild/aix-ppc64": "0.24.2",
 | 
			
		||||
        "@esbuild/android-arm": "0.24.2",
 | 
			
		||||
        "@esbuild/android-arm64": "0.24.2",
 | 
			
		||||
        "@esbuild/android-x64": "0.24.2",
 | 
			
		||||
        "@esbuild/darwin-arm64": "0.24.2",
 | 
			
		||||
        "@esbuild/darwin-x64": "0.24.2",
 | 
			
		||||
        "@esbuild/freebsd-arm64": "0.24.2",
 | 
			
		||||
        "@esbuild/freebsd-x64": "0.24.2",
 | 
			
		||||
        "@esbuild/linux-arm": "0.24.2",
 | 
			
		||||
        "@esbuild/linux-arm64": "0.24.2",
 | 
			
		||||
        "@esbuild/linux-ia32": "0.24.2",
 | 
			
		||||
        "@esbuild/linux-loong64": "0.24.2",
 | 
			
		||||
        "@esbuild/linux-mips64el": "0.24.2",
 | 
			
		||||
        "@esbuild/linux-ppc64": "0.24.2",
 | 
			
		||||
        "@esbuild/linux-riscv64": "0.24.2",
 | 
			
		||||
        "@esbuild/linux-s390x": "0.24.2",
 | 
			
		||||
        "@esbuild/linux-x64": "0.24.2",
 | 
			
		||||
        "@esbuild/netbsd-arm64": "0.24.2",
 | 
			
		||||
        "@esbuild/netbsd-x64": "0.24.2",
 | 
			
		||||
        "@esbuild/openbsd-arm64": "0.24.2",
 | 
			
		||||
        "@esbuild/openbsd-x64": "0.24.2",
 | 
			
		||||
        "@esbuild/sunos-x64": "0.24.2",
 | 
			
		||||
        "@esbuild/win32-arm64": "0.24.2",
 | 
			
		||||
        "@esbuild/win32-ia32": "0.24.2",
 | 
			
		||||
        "@esbuild/win32-x64": "0.24.2"
 | 
			
		||||
        "@esbuild/aix-ppc64": "0.25.3",
 | 
			
		||||
        "@esbuild/android-arm": "0.25.3",
 | 
			
		||||
        "@esbuild/android-arm64": "0.25.3",
 | 
			
		||||
        "@esbuild/android-x64": "0.25.3",
 | 
			
		||||
        "@esbuild/darwin-arm64": "0.25.3",
 | 
			
		||||
        "@esbuild/darwin-x64": "0.25.3",
 | 
			
		||||
        "@esbuild/freebsd-arm64": "0.25.3",
 | 
			
		||||
        "@esbuild/freebsd-x64": "0.25.3",
 | 
			
		||||
        "@esbuild/linux-arm": "0.25.3",
 | 
			
		||||
        "@esbuild/linux-arm64": "0.25.3",
 | 
			
		||||
        "@esbuild/linux-ia32": "0.25.3",
 | 
			
		||||
        "@esbuild/linux-loong64": "0.25.3",
 | 
			
		||||
        "@esbuild/linux-mips64el": "0.25.3",
 | 
			
		||||
        "@esbuild/linux-ppc64": "0.25.3",
 | 
			
		||||
        "@esbuild/linux-riscv64": "0.25.3",
 | 
			
		||||
        "@esbuild/linux-s390x": "0.25.3",
 | 
			
		||||
        "@esbuild/linux-x64": "0.25.3",
 | 
			
		||||
        "@esbuild/netbsd-arm64": "0.25.3",
 | 
			
		||||
        "@esbuild/netbsd-x64": "0.25.3",
 | 
			
		||||
        "@esbuild/openbsd-arm64": "0.25.3",
 | 
			
		||||
        "@esbuild/openbsd-x64": "0.25.3",
 | 
			
		||||
        "@esbuild/sunos-x64": "0.25.3",
 | 
			
		||||
        "@esbuild/win32-arm64": "0.25.3",
 | 
			
		||||
        "@esbuild/win32-ia32": "0.25.3",
 | 
			
		||||
        "@esbuild/win32-x64": "0.25.3"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/escape-string-regexp": {
 | 
			
		||||
@@ -1458,6 +2128,21 @@
 | 
			
		||||
        "node": ">= 6"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/fsevents": {
 | 
			
		||||
      "version": "2.3.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
 | 
			
		||||
      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "hasInstallScript": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "darwin"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/function-bind": {
 | 
			
		||||
      "version": "1.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
 | 
			
		||||
@@ -2788,7 +3473,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/postcss": {
 | 
			
		||||
      "version": "8.5.1",
 | 
			
		||||
      "version": "8.5.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
 | 
			
		||||
      "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "funding": [
 | 
			
		||||
        {
 | 
			
		||||
@@ -3256,11 +3943,13 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/rollup": {
 | 
			
		||||
      "version": "4.30.1",
 | 
			
		||||
      "version": "4.40.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz",
 | 
			
		||||
      "integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@types/estree": "1.0.6"
 | 
			
		||||
        "@types/estree": "1.0.7"
 | 
			
		||||
      },
 | 
			
		||||
      "bin": {
 | 
			
		||||
        "rollup": "dist/bin/rollup"
 | 
			
		||||
@@ -3270,25 +3959,26 @@
 | 
			
		||||
        "npm": ">=8.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "optionalDependencies": {
 | 
			
		||||
        "@rollup/rollup-android-arm-eabi": "4.30.1",
 | 
			
		||||
        "@rollup/rollup-android-arm64": "4.30.1",
 | 
			
		||||
        "@rollup/rollup-darwin-arm64": "4.30.1",
 | 
			
		||||
        "@rollup/rollup-darwin-x64": "4.30.1",
 | 
			
		||||
        "@rollup/rollup-freebsd-arm64": "4.30.1",
 | 
			
		||||
        "@rollup/rollup-freebsd-x64": "4.30.1",
 | 
			
		||||
        "@rollup/rollup-linux-arm-gnueabihf": "4.30.1",
 | 
			
		||||
        "@rollup/rollup-linux-arm-musleabihf": "4.30.1",
 | 
			
		||||
        "@rollup/rollup-linux-arm64-gnu": "4.30.1",
 | 
			
		||||
        "@rollup/rollup-linux-arm64-musl": "4.30.1",
 | 
			
		||||
        "@rollup/rollup-linux-loongarch64-gnu": "4.30.1",
 | 
			
		||||
        "@rollup/rollup-linux-powerpc64le-gnu": "4.30.1",
 | 
			
		||||
        "@rollup/rollup-linux-riscv64-gnu": "4.30.1",
 | 
			
		||||
        "@rollup/rollup-linux-s390x-gnu": "4.30.1",
 | 
			
		||||
        "@rollup/rollup-linux-x64-gnu": "4.30.1",
 | 
			
		||||
        "@rollup/rollup-linux-x64-musl": "4.30.1",
 | 
			
		||||
        "@rollup/rollup-win32-arm64-msvc": "4.30.1",
 | 
			
		||||
        "@rollup/rollup-win32-ia32-msvc": "4.30.1",
 | 
			
		||||
        "@rollup/rollup-win32-x64-msvc": "4.30.1",
 | 
			
		||||
        "@rollup/rollup-android-arm-eabi": "4.40.1",
 | 
			
		||||
        "@rollup/rollup-android-arm64": "4.40.1",
 | 
			
		||||
        "@rollup/rollup-darwin-arm64": "4.40.1",
 | 
			
		||||
        "@rollup/rollup-darwin-x64": "4.40.1",
 | 
			
		||||
        "@rollup/rollup-freebsd-arm64": "4.40.1",
 | 
			
		||||
        "@rollup/rollup-freebsd-x64": "4.40.1",
 | 
			
		||||
        "@rollup/rollup-linux-arm-gnueabihf": "4.40.1",
 | 
			
		||||
        "@rollup/rollup-linux-arm-musleabihf": "4.40.1",
 | 
			
		||||
        "@rollup/rollup-linux-arm64-gnu": "4.40.1",
 | 
			
		||||
        "@rollup/rollup-linux-arm64-musl": "4.40.1",
 | 
			
		||||
        "@rollup/rollup-linux-loongarch64-gnu": "4.40.1",
 | 
			
		||||
        "@rollup/rollup-linux-powerpc64le-gnu": "4.40.1",
 | 
			
		||||
        "@rollup/rollup-linux-riscv64-gnu": "4.40.1",
 | 
			
		||||
        "@rollup/rollup-linux-riscv64-musl": "4.40.1",
 | 
			
		||||
        "@rollup/rollup-linux-s390x-gnu": "4.40.1",
 | 
			
		||||
        "@rollup/rollup-linux-x64-gnu": "4.40.1",
 | 
			
		||||
        "@rollup/rollup-linux-x64-musl": "4.40.1",
 | 
			
		||||
        "@rollup/rollup-win32-arm64-msvc": "4.40.1",
 | 
			
		||||
        "@rollup/rollup-win32-ia32-msvc": "4.40.1",
 | 
			
		||||
        "@rollup/rollup-win32-x64-msvc": "4.40.1",
 | 
			
		||||
        "fsevents": "~2.3.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
@@ -3451,6 +4141,51 @@
 | 
			
		||||
      "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/tinyglobby": {
 | 
			
		||||
      "version": "0.2.13",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
 | 
			
		||||
      "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "fdir": "^6.4.4",
 | 
			
		||||
        "picomatch": "^4.0.2"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=12.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "url": "https://github.com/sponsors/SuperchupuDev"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/tinyglobby/node_modules/fdir": {
 | 
			
		||||
      "version": "6.4.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
 | 
			
		||||
      "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "picomatch": "^3 || ^4"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependenciesMeta": {
 | 
			
		||||
        "picomatch": {
 | 
			
		||||
          "optional": true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/tinyglobby/node_modules/picomatch": {
 | 
			
		||||
      "version": "4.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=12"
 | 
			
		||||
      },
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "url": "https://github.com/sponsors/jonschlinkert"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/to-regex-range": {
 | 
			
		||||
      "version": "5.0.1",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
@@ -3988,13 +4723,18 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/vite": {
 | 
			
		||||
      "version": "6.0.7",
 | 
			
		||||
      "version": "6.3.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz",
 | 
			
		||||
      "integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "esbuild": "^0.24.2",
 | 
			
		||||
        "postcss": "^8.4.49",
 | 
			
		||||
        "rollup": "^4.23.0"
 | 
			
		||||
        "esbuild": "^0.25.0",
 | 
			
		||||
        "fdir": "^6.4.4",
 | 
			
		||||
        "picomatch": "^4.0.2",
 | 
			
		||||
        "postcss": "^8.5.3",
 | 
			
		||||
        "rollup": "^4.34.9",
 | 
			
		||||
        "tinyglobby": "^0.2.13"
 | 
			
		||||
      },
 | 
			
		||||
      "bin": {
 | 
			
		||||
        "vite": "bin/vite.js"
 | 
			
		||||
@@ -4057,6 +4797,34 @@
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/vite/node_modules/fdir": {
 | 
			
		||||
      "version": "6.4.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
 | 
			
		||||
      "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "picomatch": "^3 || ^4"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependenciesMeta": {
 | 
			
		||||
        "picomatch": {
 | 
			
		||||
          "optional": true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/vite/node_modules/picomatch": {
 | 
			
		||||
      "version": "4.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=12"
 | 
			
		||||
      },
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "url": "https://github.com/sponsors/jonschlinkert"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/void-elements": {
 | 
			
		||||
      "version": "3.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
 | 
			
		||||
 
 | 
			
		||||
@@ -43,6 +43,6 @@
 | 
			
		||||
    "prettier": "3.5.3",
 | 
			
		||||
    "typescript": "~5.8.3",
 | 
			
		||||
    "typescript-eslint": "^8.18.2",
 | 
			
		||||
    "vite": "^6.0.5"
 | 
			
		||||
    "vite": "^6.3.4"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										4
									
								
								frontend/src/index.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								frontend/src/index.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
span,
 | 
			
		||||
p {
 | 
			
		||||
  word-break: break-word;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										26
									
								
								frontend/src/lib/hooks/use-is-mounted.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								frontend/src/lib/hooks/use-is-mounted.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
import { useCallback, useEffect, useRef } from 'react'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Custom hook that determines if the component is currently mounted.
 | 
			
		||||
 * @returns {() => boolean} A function that returns a boolean value indicating whether the component is mounted.
 | 
			
		||||
 * @public
 | 
			
		||||
 * @see [Documentation](https://usehooks-ts.com/react-hook/use-is-mounted)
 | 
			
		||||
 * @example
 | 
			
		||||
 * ```tsx
 | 
			
		||||
 * const isComponentMounted = useIsMounted();
 | 
			
		||||
 * // Use isComponentMounted() to check if the component is currently mounted before performing certain actions.
 | 
			
		||||
 * ```
 | 
			
		||||
 */
 | 
			
		||||
export function useIsMounted(): () => boolean {
 | 
			
		||||
  const isMounted = useRef(false)
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    isMounted.current = true
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      isMounted.current = false
 | 
			
		||||
    }
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  return useCallback(() => isMounted.current, [])
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Enter your TOTP code",
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "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>.",
 | 
			
		||||
    "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?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "أدخل رمز TOTP الخاص بك",
 | 
			
		||||
    "unauthorizedTitle": "غير مرخص",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "المستخدم الذي يحمل اسم المستخدم <Code>{{username}}</Code> غير مصرح له بالوصول إلى المورد <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "المستخدم الذي يحمل اسم المستخدم <Code>{{username}}</Code> غير مصرح له بتسجيل الدخول.",
 | 
			
		||||
    "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>.",
 | 
			
		||||
    "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?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Enter your TOTP code",
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "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>.",
 | 
			
		||||
    "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?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Enter your TOTP code",
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "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>.",
 | 
			
		||||
    "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?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Enter your TOTP code",
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "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>.",
 | 
			
		||||
    "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?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Geben Sie Ihren TOTP Code ein",
 | 
			
		||||
    "unauthorizedTitle": "Unautorisiert",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "Der Benutzer mit Benutzername <Code>{{username}}</Code> ist nicht berechtigt auf die Ressource <Code>{{resource}}</Code> zuzugreifen.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "Der Benutzer mit dem Benutzernamen <Code>{{username}}</Code> ist nicht berechtigt, sich einzuloggen.",
 | 
			
		||||
    "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>.",
 | 
			
		||||
    "unauthorizedButton": "Erneut versuchen",
 | 
			
		||||
    "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"
 | 
			
		||||
    "untrustedRedirectTitle": "Nicht vertrauenswürdige Weiterleitung",
 | 
			
		||||
    "untrustedRedirectSubtitle": "Sie versuchen auf eine Domain umzuleiten, die nicht mit Ihrer konfigurierten Domain übereinstimmt (<Code>{{domain}}</Code>). Sind Sie sicher, dass Sie fortfahren möchten?",
 | 
			
		||||
    "cancelTitle": "Abbrechen",
 | 
			
		||||
    "forgotPasswordTitle": "Passwort vergessen?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Εισάγετε τον κωδικό TOTP",
 | 
			
		||||
    "unauthorizedTitle": "Μη εξουσιοδοτημένο",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "Ο χρήστης με όνομα χρήστη <Code>{{username}}</Code> δεν έχει άδεια πρόσβασης στον πόρο <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "Ο χρήστης με όνομα χρήστη <Code>{{username}}</Code> δεν είναι εξουσιοδοτημένος να συνδεθεί.",
 | 
			
		||||
    "unauthorizedLoginSubtitle": "Ο χρήστης με όνομα χρήστη <Code>{{username}}</Code> δεν είναι εξουσιοδοτημένος να συνδεθεί.",
 | 
			
		||||
    "unauthorizedGroupsSubtitle": "Ο χρήστης με όνομα χρήστη <Code>{{username}}</Code> δεν είναι στις ομάδες που απαιτούνται από τον πόρο <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unauthorizedButton": "Προσπαθήστε ξανά",
 | 
			
		||||
    "untrustedRedirectTitle": "Μη έμπιστη ανακατεύθυνση",
 | 
			
		||||
    "untrustedRedirectSubtitle": "Προσπαθείτε να ανακατευθύνετε σε έναν τομέα που δεν ταιριάζει με τον ρυθμισμένο τομέα σας (<Code>{{domain}}</Code>). Είστε βέβαιοι ότι θέλετε να συνεχίσετε;",
 | 
			
		||||
    "cancelTitle": "Ακύρωση"
 | 
			
		||||
    "cancelTitle": "Ακύρωση",
 | 
			
		||||
    "forgotPasswordTitle": "Ξεχάσατε το συνθηματικό σας;"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,7 +41,8 @@
 | 
			
		||||
    "totpTitle": "Enter your TOTP code",
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "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>.",
 | 
			
		||||
    "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?",
 | 
			
		||||
 
 | 
			
		||||
@@ -41,7 +41,8 @@
 | 
			
		||||
    "totpTitle": "Enter your TOTP code",
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "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>.",
 | 
			
		||||
    "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?",
 | 
			
		||||
 
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Enter your TOTP code",
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "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>.",
 | 
			
		||||
    "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?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Enter your TOTP code",
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "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>.",
 | 
			
		||||
    "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?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Saisissez votre code TOTP",
 | 
			
		||||
    "unauthorizedTitle": "Non autorisé",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "L'utilisateur avec le nom d'utilisateur <Code>{{username}}</Code> n'est pas autorisé à accéder à la ressource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "L'utilisateur avec le nom d'utilisateur <Code>{{username}}</Code> n'est pas autorisé à se connecter.",
 | 
			
		||||
    "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>.",
 | 
			
		||||
    "unauthorizedButton": "Réessayer",
 | 
			
		||||
    "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?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Enter your TOTP code",
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "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>.",
 | 
			
		||||
    "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?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Enter your TOTP code",
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "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>.",
 | 
			
		||||
    "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?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Enter your TOTP code",
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "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>.",
 | 
			
		||||
    "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?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Enter your TOTP code",
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "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>.",
 | 
			
		||||
    "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?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Enter your TOTP code",
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "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>.",
 | 
			
		||||
    "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?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Voer je TOTP-code in",
 | 
			
		||||
    "unauthorizedTitle": "Ongeautoriseerd",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "De gebruiker met gebruikersnaam <Code>{{username}}</Code> heeft geen toegang tot de bron <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "De gebruiker met gebruikersnaam <Code>{{username}}</Code> is niet gemachtigd om in te loggen.",
 | 
			
		||||
    "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>.",
 | 
			
		||||
    "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?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Enter your TOTP code",
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "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>.",
 | 
			
		||||
    "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?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Wprowadź kod TOTP",
 | 
			
		||||
    "unauthorizedTitle": "Nieautoryzowany",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "Użytkownik o nazwie <Code>{{username}}</Code> nie jest upoważniony do uzyskania dostępu do zasobu <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "Użytkownik o nazwie <Code>{{username}}</Code> nie jest upoważniony do logowania.",
 | 
			
		||||
    "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>.",
 | 
			
		||||
    "unauthorizedButton": "Spróbuj ponownie",
 | 
			
		||||
    "untrustedRedirectTitle": "Niezaufane przekierowanie",
 | 
			
		||||
    "untrustedRedirectSubtitle": "Próbujesz przekierować do domeny, która nie pasuje do skonfigurowanej przez Ciebie domeny (<Code>{{domain}}</Code>). Czy na pewno chcesz kontynuować?",
 | 
			
		||||
    "cancelTitle": "Anuluj"
 | 
			
		||||
    "cancelTitle": "Anuluj",
 | 
			
		||||
    "forgotPasswordTitle": "Nie pamiętasz hasła?"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
    "loginTitle": "Bem-vindo de volta, faça o login com",
 | 
			
		||||
    "loginTitle": "Bem-vindo de volta, acesse com",
 | 
			
		||||
    "loginDivider": "Ou continuar com uma senha",
 | 
			
		||||
    "loginUsername": "Nome de usuário",
 | 
			
		||||
    "loginPassword": "Senha",
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Insira o seu código TOTP",
 | 
			
		||||
    "unauthorizedTitle": "Não autorizado",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "O usuário com nome de usuário <Code>{{username}}</Code> não está autorizado a acessar o recurso <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "O usuário com o nome <Code>{{username}}</Code> não está autorizado a acessar.",
 | 
			
		||||
    "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>.",
 | 
			
		||||
    "unauthorizedButton": "Tentar novamente",
 | 
			
		||||
    "untrustedRedirectTitle": "Redirecionamento não confiável",
 | 
			
		||||
    "untrustedRedirectSubtitle": "Você está tentando redirecionar para um domínio que não corresponde ao seu domínio configurado (<Code>{{domain}}</Code>). Tem certeza que deseja continuar?",
 | 
			
		||||
    "cancelTitle": "Cancelar"
 | 
			
		||||
    "cancelTitle": "Cancelar",
 | 
			
		||||
    "forgotPasswordTitle": "Esqueceu sua senha?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Enter your TOTP code",
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "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>.",
 | 
			
		||||
    "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?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Enter your TOTP code",
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "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>.",
 | 
			
		||||
    "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?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Enter your TOTP code",
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "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>.",
 | 
			
		||||
    "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?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Enter your TOTP code",
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "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>.",
 | 
			
		||||
    "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?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Enter your TOTP code",
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "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>.",
 | 
			
		||||
    "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?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Enter your TOTP code",
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "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>.",
 | 
			
		||||
    "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?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Enter your TOTP code",
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "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>.",
 | 
			
		||||
    "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?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Enter your TOTP code",
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "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>.",
 | 
			
		||||
    "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?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "输入您的 TOTP 代码",
 | 
			
		||||
    "unauthorizedTitle": "未授权",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "用户 <Code>{{username}}</Code> 无权访问资源 <Code>{{resource}}</Code>。",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "用户名 <Code>{{username}}</Code> 无登录权限。",
 | 
			
		||||
    "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>.",
 | 
			
		||||
    "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": "Cancel"
 | 
			
		||||
    "cancelTitle": "Cancel",
 | 
			
		||||
    "forgotPasswordTitle": "Forgot your password?"
 | 
			
		||||
}
 | 
			
		||||
@@ -41,9 +41,11 @@
 | 
			
		||||
    "totpTitle": "Enter your TOTP code",
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "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>.",
 | 
			
		||||
    "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?"
 | 
			
		||||
}
 | 
			
		||||
@@ -19,6 +19,7 @@ import { TotpPage } from "./pages/totp-page.tsx";
 | 
			
		||||
import { AppContextProvider } from "./context/app-context.tsx";
 | 
			
		||||
import "./lib/i18n/i18n.ts";
 | 
			
		||||
import { ForgotPasswordPage } from "./pages/forgot-password-page.tsx";
 | 
			
		||||
import "./index.css";
 | 
			
		||||
 | 
			
		||||
const queryClient = new QueryClient();
 | 
			
		||||
 | 
			
		||||
@@ -38,7 +39,10 @@ createRoot(document.getElementById("root")!).render(
 | 
			
		||||
                <Route path="/continue" element={<ContinuePage />} />
 | 
			
		||||
                <Route path="/unauthorized" element={<UnauthorizedPage />} />
 | 
			
		||||
                <Route path="/error" element={<InternalServerError />} />
 | 
			
		||||
                <Route path="/forgot-password" element={<ForgotPasswordPage />} />
 | 
			
		||||
                <Route
 | 
			
		||||
                  path="/forgot-password"
 | 
			
		||||
                  element={<ForgotPasswordPage />}
 | 
			
		||||
                />
 | 
			
		||||
                <Route path="*" element={<NotFoundPage />} />
 | 
			
		||||
              </Routes>
 | 
			
		||||
            </BrowserRouter>
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import { Navigate } from "react-router";
 | 
			
		||||
import { useUserContext } from "../context/user-context";
 | 
			
		||||
import { Layout } from "../components/layouts/layout";
 | 
			
		||||
import { ReactNode } from "react";
 | 
			
		||||
import { escapeRegex, isQueryValid } from "../utils/utils";
 | 
			
		||||
import { escapeRegex, isValidRedirectUri } from "../utils/utils";
 | 
			
		||||
import { useAppContext } from "../context/app-context";
 | 
			
		||||
import { Trans, useTranslation } from "react-i18next";
 | 
			
		||||
 | 
			
		||||
@@ -21,7 +21,7 @@ export const ContinuePage = () => {
 | 
			
		||||
    return <Navigate to={`/login?redirect_uri=${redirectUri}`} />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!isQueryValid(redirectUri)) {
 | 
			
		||||
  if (!isValidRedirectUri(redirectUri)) {
 | 
			
		||||
    return <Navigate to="/" />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -51,7 +51,7 @@ export const ContinuePage = () => {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const regex = new RegExp(`^.*${escapeRegex(domain)}$`)
 | 
			
		||||
  const regex = new RegExp(`^.*${escapeRegex(domain)}$`);
 | 
			
		||||
 | 
			
		||||
  if (!regex.test(uri.hostname)) {
 | 
			
		||||
    return (
 | 
			
		||||
@@ -66,13 +66,18 @@ export const ContinuePage = () => {
 | 
			
		||||
          values={{ domain: domain }}
 | 
			
		||||
        />
 | 
			
		||||
        <Button fullWidth mt="xl" color="red" onClick={redirect}>
 | 
			
		||||
          {t('continueTitle')}
 | 
			
		||||
          {t("continueTitle")}
 | 
			
		||||
        </Button>
 | 
			
		||||
        <Button fullWidth mt="sm" color="gray" onClick={() => window.location.href = "/"}>
 | 
			
		||||
          {t('cancelTitle')}
 | 
			
		||||
        <Button
 | 
			
		||||
          fullWidth
 | 
			
		||||
          mt="xs"
 | 
			
		||||
          color="gray"
 | 
			
		||||
          onClick={() => (window.location.href = "/")}
 | 
			
		||||
        >
 | 
			
		||||
          {t("cancelTitle")}
 | 
			
		||||
        </Button>
 | 
			
		||||
      </ContinuePageLayout>
 | 
			
		||||
    )
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (disableContinue) {
 | 
			
		||||
@@ -103,8 +108,13 @@ export const ContinuePage = () => {
 | 
			
		||||
        <Button fullWidth mt="xl" color="yellow" onClick={redirect}>
 | 
			
		||||
          {t("continueTitle")}
 | 
			
		||||
        </Button>
 | 
			
		||||
        <Button fullWidth mt="sm" color="gray" onClick={() => window.location.href = "/"}>
 | 
			
		||||
          {t('cancelTitle')}
 | 
			
		||||
        <Button
 | 
			
		||||
          fullWidth
 | 
			
		||||
          mt="xs"
 | 
			
		||||
          color="gray"
 | 
			
		||||
          onClick={() => (window.location.href = "/")}
 | 
			
		||||
        >
 | 
			
		||||
          {t("cancelTitle")}
 | 
			
		||||
        </Button>
 | 
			
		||||
      </ContinuePageLayout>
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -8,9 +8,11 @@ import { Layout } from "../components/layouts/layout";
 | 
			
		||||
import { OAuthButtons } from "../components/auth/oauth-buttons";
 | 
			
		||||
import { LoginFormValues } from "../schemas/login-schema";
 | 
			
		||||
import { LoginForm } from "../components/auth/login-forn";
 | 
			
		||||
import { isQueryValid } from "../utils/utils";
 | 
			
		||||
import { useAppContext } from "../context/app-context";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import { useIsMounted } from "../lib/hooks/use-is-mounted";
 | 
			
		||||
import { isValidRedirectUri } from "../utils/utils";
 | 
			
		||||
 | 
			
		||||
export const LoginPage = () => {
 | 
			
		||||
  const queryString = window.location.search;
 | 
			
		||||
@@ -18,16 +20,29 @@ export const LoginPage = () => {
 | 
			
		||||
  const redirectUri = params.get("redirect_uri") ?? "";
 | 
			
		||||
 | 
			
		||||
  const { isLoggedIn } = useUserContext();
 | 
			
		||||
  const { configuredProviders, title, genericName } = useAppContext();
 | 
			
		||||
 | 
			
		||||
  if (isLoggedIn) {
 | 
			
		||||
    return <Navigate to="/logout" />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
    configuredProviders,
 | 
			
		||||
    title,
 | 
			
		||||
    genericName,
 | 
			
		||||
    oauthAutoRedirect: oauthAutoRedirectContext,
 | 
			
		||||
  } = useAppContext();
 | 
			
		||||
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
  const [oauthAutoRedirect, setOAuthAutoRedirect] = useState(
 | 
			
		||||
    oauthAutoRedirectContext,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const oauthProviders = configuredProviders.filter(
 | 
			
		||||
    (value) => value !== "username",
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (isLoggedIn) {
 | 
			
		||||
    return <Navigate to="/logout" />;
 | 
			
		||||
  }
 | 
			
		||||
  const isMounted = useIsMounted();
 | 
			
		||||
 | 
			
		||||
  const loginMutation = useMutation({
 | 
			
		||||
    mutationFn: (login: LoginFormValues) => {
 | 
			
		||||
@@ -63,7 +78,7 @@ export const LoginPage = () => {
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        if (!isQueryValid(redirectUri)) {
 | 
			
		||||
        if (!isValidRedirectUri(redirectUri)) {
 | 
			
		||||
          window.location.replace("/");
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
@@ -85,6 +100,7 @@ export const LoginPage = () => {
 | 
			
		||||
        message: t("loginOauthFailSubtitle"),
 | 
			
		||||
        color: "red",
 | 
			
		||||
      });
 | 
			
		||||
      setOAuthAutoRedirect("none");
 | 
			
		||||
    },
 | 
			
		||||
    onSuccess: (data) => {
 | 
			
		||||
      notifications.show({
 | 
			
		||||
@@ -102,6 +118,33 @@ export const LoginPage = () => {
 | 
			
		||||
    loginMutation.mutate(values);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (isMounted()) {
 | 
			
		||||
      if (
 | 
			
		||||
        oauthProviders.includes(oauthAutoRedirect) &&
 | 
			
		||||
        isValidRedirectUri(redirectUri)
 | 
			
		||||
      ) {
 | 
			
		||||
        loginOAuthMutation.mutate(oauthAutoRedirect);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  if (
 | 
			
		||||
    oauthProviders.includes(oauthAutoRedirect) &&
 | 
			
		||||
    isValidRedirectUri(redirectUri)
 | 
			
		||||
  ) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Layout>
 | 
			
		||||
        <Paper shadow="md" p="xl" mt={30} radius="md" withBorder>
 | 
			
		||||
          <Text size="xl" fw={700}>
 | 
			
		||||
            {t("continueRedirectingTitle")}
 | 
			
		||||
          </Text>
 | 
			
		||||
          <Text>{t("loginOauthSuccessSubtitle")}</Text>
 | 
			
		||||
        </Paper>
 | 
			
		||||
      </Layout>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Layout>
 | 
			
		||||
      <Title ta="center">{title}</Title>
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ import { useAppContext } from "../context/app-context";
 | 
			
		||||
import { Trans, useTranslation } from "react-i18next";
 | 
			
		||||
 | 
			
		||||
export const LogoutPage = () => {
 | 
			
		||||
  const { isLoggedIn, username, oauth, provider } = useUserContext();
 | 
			
		||||
  const { isLoggedIn, oauth, provider, email, username } = useUserContext();
 | 
			
		||||
  const { genericName } = useAppContext();
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
@@ -56,7 +56,7 @@ export const LogoutPage = () => {
 | 
			
		||||
              values={{
 | 
			
		||||
                provider:
 | 
			
		||||
                  provider === "generic" ? genericName : capitalize(provider),
 | 
			
		||||
                username: username,
 | 
			
		||||
                username: email,
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          ) : (
 | 
			
		||||
 
 | 
			
		||||
@@ -1,48 +1,90 @@
 | 
			
		||||
import { Button, Code, Paper, Text } from "@mantine/core";
 | 
			
		||||
import { Layout } from "../components/layouts/layout";
 | 
			
		||||
import { Navigate } from "react-router";
 | 
			
		||||
import { isQueryValid } from "../utils/utils";
 | 
			
		||||
import { Trans, useTranslation } from "react-i18next";
 | 
			
		||||
import React, { useEffect } from "react";
 | 
			
		||||
import { isValidQuery } from "../utils/utils";
 | 
			
		||||
import { useIsMounted } from "../lib/hooks/use-is-mounted";
 | 
			
		||||
 | 
			
		||||
export const UnauthorizedPage = () => {
 | 
			
		||||
  const queryString = window.location.search;
 | 
			
		||||
  const params = new URLSearchParams(queryString);
 | 
			
		||||
  const username = params.get("username") ?? "";
 | 
			
		||||
  const groupErr = params.get("groupErr") ?? "";
 | 
			
		||||
  const resource = params.get("resource") ?? "";
 | 
			
		||||
 | 
			
		||||
  const [isGroupErr, setIsGroupErr] = React.useState(false);
 | 
			
		||||
 | 
			
		||||
  const useMounted = useIsMounted();
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (useMounted()) {
 | 
			
		||||
      if (isValidQuery(groupErr)) {
 | 
			
		||||
        if (groupErr === "true") {
 | 
			
		||||
          setIsGroupErr(true);
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        setIsGroupErr(false);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      setIsGroupErr(false);
 | 
			
		||||
    }
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
  if (!isQueryValid(username)) {
 | 
			
		||||
  if (!isValidQuery(username)) {
 | 
			
		||||
    return <Navigate to="/" />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (isValidQuery(resource) && !isGroupErr) {
 | 
			
		||||
    return (
 | 
			
		||||
      <UnauthorizedLayout>
 | 
			
		||||
        <Trans
 | 
			
		||||
          i18nKey="unauthorizedResourceSubtitle"
 | 
			
		||||
          t={t}
 | 
			
		||||
          components={{ Code: <Code /> }}
 | 
			
		||||
          values={{ resource, username }}
 | 
			
		||||
        />
 | 
			
		||||
      </UnauthorizedLayout>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (isGroupErr && isValidQuery(resource)) {
 | 
			
		||||
    return (
 | 
			
		||||
      <UnauthorizedLayout>
 | 
			
		||||
        <Trans
 | 
			
		||||
          i18nKey="unauthorizedGroupsSubtitle"
 | 
			
		||||
          t={t}
 | 
			
		||||
          components={{ Code: <Code /> }}
 | 
			
		||||
          values={{ username, resource }}
 | 
			
		||||
        />
 | 
			
		||||
      </UnauthorizedLayout>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <UnauthorizedLayout>
 | 
			
		||||
      <Trans
 | 
			
		||||
        i18nKey="unauthorizedLoginSubtitle"
 | 
			
		||||
        t={t}
 | 
			
		||||
        components={{ Code: <Code /> }}
 | 
			
		||||
        values={{ username }}
 | 
			
		||||
      />
 | 
			
		||||
    </UnauthorizedLayout>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const UnauthorizedLayout = ({ children }: { children: React.ReactNode }) => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Layout>
 | 
			
		||||
      <Paper shadow="md" p={30} mt={30} radius="md" withBorder>
 | 
			
		||||
        <Text size="xl" fw={700}>
 | 
			
		||||
          {t("Unauthorized")}
 | 
			
		||||
        </Text>
 | 
			
		||||
        <Text>
 | 
			
		||||
          {isQueryValid(resource) ? (
 | 
			
		||||
            <Text>
 | 
			
		||||
              <Trans
 | 
			
		||||
                i18nKey="unauthorizedResourceSubtitle"
 | 
			
		||||
                t={t}
 | 
			
		||||
                components={{ Code: <Code /> }}
 | 
			
		||||
                values={{ resource, username }}
 | 
			
		||||
              />
 | 
			
		||||
            </Text>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Text>
 | 
			
		||||
              <Trans
 | 
			
		||||
                i18nKey="unaothorizedLoginSubtitle"
 | 
			
		||||
                t={t}
 | 
			
		||||
                components={{ Code: <Code /> }}
 | 
			
		||||
                values={{ username }}
 | 
			
		||||
              />
 | 
			
		||||
            </Text>
 | 
			
		||||
          )}
 | 
			
		||||
        </Text>
 | 
			
		||||
        <Text>{children}</Text>
 | 
			
		||||
        <Button
 | 
			
		||||
          fullWidth
 | 
			
		||||
          mt="xl"
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ export const appContextSchema = z.object({
 | 
			
		||||
  genericName: z.string(),
 | 
			
		||||
  domain: z.string(),
 | 
			
		||||
  forgotPasswordMessage: z.string(),
 | 
			
		||||
  oauthAutoRedirect: z.enum(["none", "github", "google", "generic"]),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export type AppContextSchemaType = z.infer<typeof appContextSchema>;
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,8 @@ import { z } from "zod";
 | 
			
		||||
export const userContextSchema = z.object({
 | 
			
		||||
  isLoggedIn: z.boolean(),
 | 
			
		||||
  username: z.string(),
 | 
			
		||||
  name: z.string(),
 | 
			
		||||
  email: z.string(),
 | 
			
		||||
  oauth: z.boolean(),
 | 
			
		||||
  provider: z.string(),
 | 
			
		||||
  totpPending: z.boolean(),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,17 @@
 | 
			
		||||
export const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
 | 
			
		||||
export const isQueryValid = (value: string) => value.trim() !== "" && value !== "null";
 | 
			
		||||
export const escapeRegex = (value: string) => value.replace(/[-\/\\^$.*+?()[\]{}|]/g, "\\$&");
 | 
			
		||||
export const isValidQuery = (query: string) => query && query.trim() !== "";
 | 
			
		||||
 | 
			
		||||
export const isValidRedirectUri = (value: string) => {
 | 
			
		||||
    if (!isValidQuery(value)) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        new URL(value);
 | 
			
		||||
    } catch {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return true;
 | 
			
		||||
}
 | 
			
		||||
@@ -12,6 +12,7 @@ export default defineConfig({
 | 
			
		||||
        changeOrigin: true,
 | 
			
		||||
        rewrite: (path) => path.replace(/^\/api/, ""),
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
    allowedHosts: true,
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -45,6 +45,11 @@ var authConfig = types.AuthConfig{
 | 
			
		||||
	LoginMaxRetries: 0,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Simple hooks config for tests
 | 
			
		||||
var hooksConfig = types.HooksConfig{
 | 
			
		||||
	Domain: "localhost",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Cookie
 | 
			
		||||
var cookie string
 | 
			
		||||
 | 
			
		||||
@@ -83,7 +88,7 @@ func getAPI(t *testing.T) *api.API {
 | 
			
		||||
	providers.Init()
 | 
			
		||||
 | 
			
		||||
	// Create hooks service
 | 
			
		||||
	hooks := hooks.NewHooks(auth, providers)
 | 
			
		||||
	hooks := hooks.NewHooks(hooksConfig, auth, providers)
 | 
			
		||||
 | 
			
		||||
	// Create handlers service
 | 
			
		||||
	handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker)
 | 
			
		||||
 
 | 
			
		||||
@@ -160,9 +160,12 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie)
 | 
			
		||||
 | 
			
		||||
	// Set data
 | 
			
		||||
	session.Values["username"] = data.Username
 | 
			
		||||
	session.Values["name"] = data.Name
 | 
			
		||||
	session.Values["email"] = data.Email
 | 
			
		||||
	session.Values["provider"] = data.Provider
 | 
			
		||||
	session.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix()
 | 
			
		||||
	session.Values["totpPending"] = data.TotpPending
 | 
			
		||||
	session.Values["oauthGroups"] = data.OAuthGroups
 | 
			
		||||
 | 
			
		||||
	// Save session
 | 
			
		||||
	err = session.Save(c.Request, c.Writer)
 | 
			
		||||
@@ -211,14 +214,24 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error)
 | 
			
		||||
		return types.SessionCookie{}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Debug().Msg("Got session")
 | 
			
		||||
 | 
			
		||||
	// Get data from session
 | 
			
		||||
	username, usernameOk := session.Values["username"].(string)
 | 
			
		||||
	email, emailOk := session.Values["email"].(string)
 | 
			
		||||
	name, nameOk := session.Values["name"].(string)
 | 
			
		||||
	provider, providerOK := session.Values["provider"].(string)
 | 
			
		||||
	expiry, expiryOk := session.Values["expiry"].(int64)
 | 
			
		||||
	totpPending, totpPendingOk := session.Values["totpPending"].(bool)
 | 
			
		||||
	oauthGroups, oauthGroupsOk := session.Values["oauthGroups"].(string)
 | 
			
		||||
 | 
			
		||||
	if !usernameOk || !providerOK || !expiryOk || !totpPendingOk {
 | 
			
		||||
		log.Warn().Msg("Session cookie is missing data")
 | 
			
		||||
	if !usernameOk || !providerOK || !expiryOk || !totpPendingOk || !emailOk || !nameOk || !oauthGroupsOk {
 | 
			
		||||
		log.Warn().Msg("Session cookie is invalid")
 | 
			
		||||
 | 
			
		||||
		// If any data is missing, delete the session cookie
 | 
			
		||||
		auth.DeleteSessionCookie(c)
 | 
			
		||||
 | 
			
		||||
		// Return empty cookie
 | 
			
		||||
		return types.SessionCookie{}, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -233,13 +246,16 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error)
 | 
			
		||||
		return types.SessionCookie{}, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Debug().Str("username", username).Str("provider", provider).Int64("expiry", expiry).Bool("totpPending", totpPending).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{
 | 
			
		||||
		Username:    username,
 | 
			
		||||
		Name:        name,
 | 
			
		||||
		Email:       email,
 | 
			
		||||
		Provider:    provider,
 | 
			
		||||
		TotpPending: totpPending,
 | 
			
		||||
		OAuthGroups: oauthGroups,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -248,48 +264,52 @@ func (auth *Auth) UserAuthConfigured() bool {
 | 
			
		||||
	return len(auth.Config.Users) > 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext) (bool, error) {
 | 
			
		||||
	// Get headers
 | 
			
		||||
	host := c.Request.Header.Get("X-Forwarded-Host")
 | 
			
		||||
 | 
			
		||||
	// Get app id
 | 
			
		||||
	appId := strings.Split(host, ".")[0]
 | 
			
		||||
 | 
			
		||||
	// Get the container labels
 | 
			
		||||
	labels, err := auth.Docker.GetLabels(appId)
 | 
			
		||||
 | 
			
		||||
	// If there is an error, return false
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext, labels types.TinyauthLabels) bool {
 | 
			
		||||
	// Check if oauth is allowed
 | 
			
		||||
	if context.OAuth {
 | 
			
		||||
		log.Debug().Msg("Checking OAuth whitelist")
 | 
			
		||||
		return utils.CheckWhitelist(labels.OAuthWhitelist, context.Username), nil
 | 
			
		||||
		return utils.CheckWhitelist(labels.OAuthWhitelist, context.Email)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Check users
 | 
			
		||||
	log.Debug().Msg("Checking users")
 | 
			
		||||
 | 
			
		||||
	return utils.CheckWhitelist(labels.Users, context.Username), nil
 | 
			
		||||
	return utils.CheckWhitelist(labels.Users, context.Username)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth *Auth) AuthEnabled(c *gin.Context) (bool, error) {
 | 
			
		||||
func (auth *Auth) OAuthGroup(c *gin.Context, context types.UserContext, labels types.TinyauthLabels) bool {
 | 
			
		||||
	// Check if groups are required
 | 
			
		||||
	if labels.OAuthGroups == "" {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Check if we are using the generic oauth provider
 | 
			
		||||
	if context.Provider != "generic" {
 | 
			
		||||
		log.Debug().Msg("Not using generic provider, skipping group check")
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Split the groups by comma (no need to parse since they are from the API response)
 | 
			
		||||
	oauthGroups := strings.Split(context.OAuthGroups, ",")
 | 
			
		||||
 | 
			
		||||
	// For every group check if it is in the required groups
 | 
			
		||||
	for _, group := range oauthGroups {
 | 
			
		||||
		if utils.CheckWhitelist(labels.OAuthGroups, group) {
 | 
			
		||||
			log.Debug().Str("group", group).Msg("Group is in required groups")
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// No groups matched
 | 
			
		||||
	log.Debug().Msg("No groups matched")
 | 
			
		||||
 | 
			
		||||
	// Return false
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth *Auth) AuthEnabled(c *gin.Context, labels types.TinyauthLabels) (bool, error) {
 | 
			
		||||
	// Get headers
 | 
			
		||||
	uri := c.Request.Header.Get("X-Forwarded-Uri")
 | 
			
		||||
	host := c.Request.Header.Get("X-Forwarded-Host")
 | 
			
		||||
 | 
			
		||||
	// Get app id
 | 
			
		||||
	appId := strings.Split(host, ".")[0]
 | 
			
		||||
 | 
			
		||||
	// Get the container labels
 | 
			
		||||
	labels, err := auth.Docker.GetLabels(appId)
 | 
			
		||||
 | 
			
		||||
	// If there is an error, auth enabled
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return true, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Check if the allowed label is empty
 | 
			
		||||
	if labels.Allowed == "" {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,4 +6,13 @@ var TinyauthLabels = []string{
 | 
			
		||||
	"tinyauth.users",
 | 
			
		||||
	"tinyauth.allowed",
 | 
			
		||||
	"tinyauth.headers",
 | 
			
		||||
	"tinyauth.oauth.groups",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Claims are the OIDC supported claims (including preferd username for some reason)
 | 
			
		||||
type Claims struct {
 | 
			
		||||
	Name              string   `json:"name"`
 | 
			
		||||
	Email             string   `json:"email"`
 | 
			
		||||
	PreferredUsername string   `json:"preferred_username"`
 | 
			
		||||
	Groups            []string `json:"groups"`
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,8 +6,7 @@ import (
 | 
			
		||||
	"tinyauth/internal/types"
 | 
			
		||||
	"tinyauth/internal/utils"
 | 
			
		||||
 | 
			
		||||
	apiTypes "github.com/docker/docker/api/types"
 | 
			
		||||
	containerTypes "github.com/docker/docker/api/types/container"
 | 
			
		||||
	container "github.com/docker/docker/api/types/container"
 | 
			
		||||
	"github.com/docker/docker/client"
 | 
			
		||||
	"github.com/rs/zerolog/log"
 | 
			
		||||
)
 | 
			
		||||
@@ -38,9 +37,9 @@ func (docker *Docker) Init() error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (docker *Docker) GetContainers() ([]apiTypes.Container, error) {
 | 
			
		||||
func (docker *Docker) GetContainers() ([]container.Summary, error) {
 | 
			
		||||
	// Get the list of containers
 | 
			
		||||
	containers, err := docker.Client.ContainerList(docker.Context, containerTypes.ListOptions{})
 | 
			
		||||
	containers, err := docker.Client.ContainerList(docker.Context, container.ListOptions{})
 | 
			
		||||
 | 
			
		||||
	// Check if there was an error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -51,13 +50,13 @@ func (docker *Docker) GetContainers() ([]apiTypes.Container, error) {
 | 
			
		||||
	return containers, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (docker *Docker) InspectContainer(containerId string) (apiTypes.ContainerJSON, error) {
 | 
			
		||||
func (docker *Docker) InspectContainer(containerId string) (container.InspectResponse, error) {
 | 
			
		||||
	// Inspect the container
 | 
			
		||||
	inspect, err := docker.Client.ContainerInspect(docker.Context, containerId)
 | 
			
		||||
 | 
			
		||||
	// Check if there was an error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return apiTypes.ContainerJSON{}, err
 | 
			
		||||
		return container.InspectResponse{}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Return the inspect
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ import (
 | 
			
		||||
	"tinyauth/internal/hooks"
 | 
			
		||||
	"tinyauth/internal/providers"
 | 
			
		||||
	"tinyauth/internal/types"
 | 
			
		||||
	"tinyauth/internal/utils"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/google/go-querystring/query"
 | 
			
		||||
@@ -68,12 +69,17 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
 | 
			
		||||
	proto := c.Request.Header.Get("X-Forwarded-Proto")
 | 
			
		||||
	host := c.Request.Header.Get("X-Forwarded-Host")
 | 
			
		||||
 | 
			
		||||
	// Check if auth is enabled
 | 
			
		||||
	authEnabled, err := h.Auth.AuthEnabled(c)
 | 
			
		||||
	// 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 check if app is allowed")
 | 
			
		||||
		log.Error().Err(err).Msg("Failed to get container labels")
 | 
			
		||||
 | 
			
		||||
		if proxy.Proxy == "nginx" || !isBrowser {
 | 
			
		||||
			c.JSON(500, gin.H{
 | 
			
		||||
@@ -87,11 +93,8 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Get the app id
 | 
			
		||||
	appId := strings.Split(host, ".")[0]
 | 
			
		||||
 | 
			
		||||
	// Get the container labels
 | 
			
		||||
	labels, err := h.Docker.GetLabels(appId)
 | 
			
		||||
	// Check if auth is enabled
 | 
			
		||||
	authEnabled, err := h.Auth.AuthEnabled(c, labels)
 | 
			
		||||
 | 
			
		||||
	// Check if there was an error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -113,7 +116,7 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
 | 
			
		||||
	if !authEnabled {
 | 
			
		||||
		for key, value := range labels.Headers {
 | 
			
		||||
			log.Debug().Str("key", key).Str("value", value).Msg("Setting header")
 | 
			
		||||
			c.Header(key, value)
 | 
			
		||||
			c.Header(key, utils.SanitizeHeader(value))
 | 
			
		||||
		}
 | 
			
		||||
		c.JSON(200, gin.H{
 | 
			
		||||
			"status":  200,
 | 
			
		||||
@@ -125,28 +128,18 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
 | 
			
		||||
	// 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, err := h.Auth.ResourceAllowed(c, userContext)
 | 
			
		||||
 | 
			
		||||
		// 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.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		appAllowed := h.Auth.ResourceAllowed(c, userContext, labels)
 | 
			
		||||
 | 
			
		||||
		log.Debug().Bool("appAllowed", appAllowed).Msg("Checking if app is allowed")
 | 
			
		||||
 | 
			
		||||
@@ -165,11 +158,20 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Build query
 | 
			
		||||
			queries, err := query.Values(types.UnauthorizedQuery{
 | 
			
		||||
				Username: userContext.Username,
 | 
			
		||||
			// 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 {
 | 
			
		||||
@@ -183,13 +185,66 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Set the user header
 | 
			
		||||
		c.Header("Remote-User", userContext.Username)
 | 
			
		||||
		// 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")
 | 
			
		||||
 | 
			
		||||
				// Set WWW-Authenticate header
 | 
			
		||||
				c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"")
 | 
			
		||||
 | 
			
		||||
				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.StatusPermanentRedirect, 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, value)
 | 
			
		||||
			c.Header(key, utils.SanitizeHeader(value))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// The user is allowed to access the app
 | 
			
		||||
@@ -310,6 +365,8 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
 | 
			
		||||
		// 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,
 | 
			
		||||
		})
 | 
			
		||||
@@ -328,6 +385,8 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
 | 
			
		||||
	// 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",
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
@@ -402,6 +461,8 @@ func (h *Handlers) TotpHandler(c *gin.Context) {
 | 
			
		||||
	// 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",
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
@@ -448,6 +509,7 @@ func (h *Handlers) AppHandler(c *gin.Context) {
 | 
			
		||||
		GenericName:           h.Config.GenericName,
 | 
			
		||||
		Domain:                h.Config.Domain,
 | 
			
		||||
		ForgotPasswordMessage: h.Config.ForgotPasswordMessage,
 | 
			
		||||
		OAuthAutoRedirect:     h.Config.OAuthAutoRedirect,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Return app context
 | 
			
		||||
@@ -465,6 +527,8 @@ func (h *Handlers) UserHandler(c *gin.Context) {
 | 
			
		||||
		Status:      200,
 | 
			
		||||
		IsLoggedIn:  userContext.IsLoggedIn,
 | 
			
		||||
		Username:    userContext.Username,
 | 
			
		||||
		Name:        userContext.Name,
 | 
			
		||||
		Email:       userContext.Email,
 | 
			
		||||
		Provider:    userContext.Provider,
 | 
			
		||||
		Oauth:       userContext.OAuth,
 | 
			
		||||
		TotpPending: userContext.TotpPending,
 | 
			
		||||
@@ -613,25 +677,32 @@ func (h *Handlers) OauthCallbackHandler(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Get email
 | 
			
		||||
	email, err := h.Providers.GetUser(providerName.Provider)
 | 
			
		||||
 | 
			
		||||
	log.Debug().Str("email", email).Msg("Got email")
 | 
			
		||||
	// Get user
 | 
			
		||||
	user, err := h.Providers.GetUser(providerName.Provider)
 | 
			
		||||
 | 
			
		||||
	// Handle error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error().Err(err).Msg("Failed to get email")
 | 
			
		||||
		log.Error().Msg("Failed to get user")
 | 
			
		||||
		c.Redirect(http.StatusPermanentRedirect, 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.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Email is not whitelisted
 | 
			
		||||
	if !h.Auth.EmailWhitelisted(email) {
 | 
			
		||||
		log.Warn().Str("email", email).Msg("Email 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: email,
 | 
			
		||||
			Username: user.Email,
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		// Handle error
 | 
			
		||||
@@ -647,10 +718,31 @@ func (h *Handlers) OauthCallbackHandler(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
	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: email,
 | 
			
		||||
		Username:    username,
 | 
			
		||||
		Name:        name,
 | 
			
		||||
		Email:       user.Email,
 | 
			
		||||
		Provider:    providerName.Provider,
 | 
			
		||||
		OAuthGroups: strings.Join(user.Groups, ","),
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	// Check if we have a redirect URI
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +1,27 @@
 | 
			
		||||
package hooks
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"tinyauth/internal/auth"
 | 
			
		||||
	"tinyauth/internal/providers"
 | 
			
		||||
	"tinyauth/internal/types"
 | 
			
		||||
	"tinyauth/internal/utils"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/rs/zerolog/log"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func NewHooks(auth *auth.Auth, providers *providers.Providers) *Hooks {
 | 
			
		||||
func NewHooks(config types.HooksConfig, auth *auth.Auth, providers *providers.Providers) *Hooks {
 | 
			
		||||
	return &Hooks{
 | 
			
		||||
		Config:    config,
 | 
			
		||||
		Auth:      auth,
 | 
			
		||||
		Providers: providers,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Hooks struct {
 | 
			
		||||
	Config    types.HooksConfig
 | 
			
		||||
	Auth      *auth.Auth
 | 
			
		||||
	Providers *providers.Providers
 | 
			
		||||
}
 | 
			
		||||
@@ -30,17 +35,27 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
 | 
			
		||||
	if basic != nil {
 | 
			
		||||
		log.Debug().Msg("Got basic auth")
 | 
			
		||||
 | 
			
		||||
		// Check if user exists and password is correct
 | 
			
		||||
		// Get user
 | 
			
		||||
		user := hooks.Auth.GetUser(basic.Username)
 | 
			
		||||
 | 
			
		||||
		if user != nil && hooks.Auth.CheckPassword(*user, basic.Password) {
 | 
			
		||||
		// 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,
 | 
			
		||||
				OAuth:       false,
 | 
			
		||||
				Provider:    "basic",
 | 
			
		||||
				TotpPending: false,
 | 
			
		||||
				TotpEnabled: user.TotpSecret != "",
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -50,13 +65,7 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error().Err(err).Msg("Failed to get session cookie")
 | 
			
		||||
		// Return empty context
 | 
			
		||||
		return types.UserContext{
 | 
			
		||||
			Username:    "",
 | 
			
		||||
			IsLoggedIn:  false,
 | 
			
		||||
			OAuth:       false,
 | 
			
		||||
			Provider:    "",
 | 
			
		||||
			TotpPending: false,
 | 
			
		||||
		}
 | 
			
		||||
		return types.UserContext{}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Check if session cookie has totp pending
 | 
			
		||||
@@ -65,8 +74,8 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
 | 
			
		||||
		// Return empty context since we are pending totp
 | 
			
		||||
		return types.UserContext{
 | 
			
		||||
			Username:    cookie.Username,
 | 
			
		||||
			IsLoggedIn:  false,
 | 
			
		||||
			OAuth:       false,
 | 
			
		||||
			Name:        cookie.Name,
 | 
			
		||||
			Email:       cookie.Email,
 | 
			
		||||
			Provider:    cookie.Provider,
 | 
			
		||||
			TotpPending: true,
 | 
			
		||||
		}
 | 
			
		||||
@@ -83,10 +92,10 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
 | 
			
		||||
			// It exists so we are logged in
 | 
			
		||||
			return types.UserContext{
 | 
			
		||||
				Username:   cookie.Username,
 | 
			
		||||
				Name:       cookie.Name,
 | 
			
		||||
				Email:      cookie.Email,
 | 
			
		||||
				IsLoggedIn: true,
 | 
			
		||||
				OAuth:       false,
 | 
			
		||||
				Provider:   "username",
 | 
			
		||||
				TotpPending: false,
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@@ -101,20 +110,14 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
 | 
			
		||||
		log.Debug().Msg("Provider exists")
 | 
			
		||||
 | 
			
		||||
		// Check if the oauth email is whitelisted
 | 
			
		||||
		if !hooks.Auth.EmailWhitelisted(cookie.Username) {
 | 
			
		||||
			log.Error().Str("email", cookie.Username).Msg("Email is not whitelisted")
 | 
			
		||||
		if !hooks.Auth.EmailWhitelisted(cookie.Email) {
 | 
			
		||||
			log.Error().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)
 | 
			
		||||
 | 
			
		||||
			// Return empty context
 | 
			
		||||
			return types.UserContext{
 | 
			
		||||
				Username:    "",
 | 
			
		||||
				IsLoggedIn:  false,
 | 
			
		||||
				OAuth:       false,
 | 
			
		||||
				Provider:    "",
 | 
			
		||||
				TotpPending: false,
 | 
			
		||||
			}
 | 
			
		||||
			return types.UserContext{}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		log.Debug().Msg("Email is whitelisted")
 | 
			
		||||
@@ -122,19 +125,15 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
 | 
			
		||||
		// Return user context since we are logged in with oauth
 | 
			
		||||
		return types.UserContext{
 | 
			
		||||
			Username:    cookie.Username,
 | 
			
		||||
			Name:        cookie.Name,
 | 
			
		||||
			Email:       cookie.Email,
 | 
			
		||||
			IsLoggedIn:  true,
 | 
			
		||||
			OAuth:       true,
 | 
			
		||||
			Provider:    cookie.Provider,
 | 
			
		||||
			TotpPending: false,
 | 
			
		||||
			OAuthGroups: cookie.OAuthGroups,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Neither basic auth or oauth is set so we return an empty context
 | 
			
		||||
	return types.UserContext{
 | 
			
		||||
		Username:    "",
 | 
			
		||||
		IsLoggedIn:  false,
 | 
			
		||||
		OAuth:       false,
 | 
			
		||||
		Provider:    "",
 | 
			
		||||
		TotpPending: false,
 | 
			
		||||
	}
 | 
			
		||||
	return types.UserContext{}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,24 +4,25 @@ import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"tinyauth/internal/constants"
 | 
			
		||||
 | 
			
		||||
	"github.com/rs/zerolog/log"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// We are assuming that the generic provider will return a JSON object with an email field
 | 
			
		||||
type GenericUserInfoResponse struct {
 | 
			
		||||
	Email string `json:"email"`
 | 
			
		||||
}
 | 
			
		||||
func GetGenericUser(client *http.Client, url string) (constants.Claims, error) {
 | 
			
		||||
	// Create user struct
 | 
			
		||||
	var user constants.Claims
 | 
			
		||||
 | 
			
		||||
func GetGenericEmail(client *http.Client, url string) (string, error) {
 | 
			
		||||
	// Using the oauth client get the user info url
 | 
			
		||||
	res, err := client.Get(url)
 | 
			
		||||
 | 
			
		||||
	// Check if there was an error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
		return user, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	defer res.Body.Close()
 | 
			
		||||
 | 
			
		||||
	log.Debug().Msg("Got response from generic provider")
 | 
			
		||||
 | 
			
		||||
	// Read the body of the response
 | 
			
		||||
@@ -29,24 +30,21 @@ func GetGenericEmail(client *http.Client, url string) (string, error) {
 | 
			
		||||
 | 
			
		||||
	// Check if there was an error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
		return user, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Debug().Msg("Read body from generic provider")
 | 
			
		||||
 | 
			
		||||
	// Parse the body into a user struct
 | 
			
		||||
	var user GenericUserInfoResponse
 | 
			
		||||
 | 
			
		||||
	// Unmarshal the body into the user struct
 | 
			
		||||
	err = json.Unmarshal(body, &user)
 | 
			
		||||
 | 
			
		||||
	// Check if there was an error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
		return user, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Debug().Msg("Parsed user from generic provider")
 | 
			
		||||
 | 
			
		||||
	// Return the email
 | 
			
		||||
	return user.Email, nil
 | 
			
		||||
	// Return the user
 | 
			
		||||
	return user, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,51 +5,96 @@ import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"tinyauth/internal/constants"
 | 
			
		||||
 | 
			
		||||
	"github.com/rs/zerolog/log"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Github has a different response than the generic provider
 | 
			
		||||
type GithubUserInfoResponse []struct {
 | 
			
		||||
// Response for the github email endpoint
 | 
			
		||||
type GithubEmailResponse []struct {
 | 
			
		||||
	Email   string `json:"email"`
 | 
			
		||||
	Primary bool   `json:"primary"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// The scopes required for the github provider
 | 
			
		||||
func GithubScopes() []string {
 | 
			
		||||
	return []string{"user:email"}
 | 
			
		||||
// Response for the github user endpoint
 | 
			
		||||
type GithubUserInfoResponse struct {
 | 
			
		||||
	Login string `json:"login"`
 | 
			
		||||
	Name  string `json:"name"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetGithubEmail(client *http.Client) (string, error) {
 | 
			
		||||
	// Get the user emails from github using the oauth http client
 | 
			
		||||
	res, err := client.Get("https://api.github.com/user/emails")
 | 
			
		||||
// The scopes required for the github provider
 | 
			
		||||
func GithubScopes() []string {
 | 
			
		||||
	return []string{"user:email", "read:user"}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetGithubUser(client *http.Client) (constants.Claims, error) {
 | 
			
		||||
	// Create user struct
 | 
			
		||||
	var user constants.Claims
 | 
			
		||||
 | 
			
		||||
	// Get the user info from github using the oauth http client
 | 
			
		||||
	res, err := client.Get("https://api.github.com/user")
 | 
			
		||||
 | 
			
		||||
	// Check if there was an error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
		return user, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Debug().Msg("Got response from github")
 | 
			
		||||
	defer res.Body.Close()
 | 
			
		||||
 | 
			
		||||
	log.Debug().Msg("Got user response from github")
 | 
			
		||||
 | 
			
		||||
	// Read the body of the response
 | 
			
		||||
	body, err := io.ReadAll(res.Body)
 | 
			
		||||
 | 
			
		||||
	// Check if there was an error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
		return user, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Debug().Msg("Read body from github")
 | 
			
		||||
	log.Debug().Msg("Read user body from github")
 | 
			
		||||
 | 
			
		||||
	// Parse the body into a user struct
 | 
			
		||||
	var emails GithubUserInfoResponse
 | 
			
		||||
	var userInfo GithubUserInfoResponse
 | 
			
		||||
 | 
			
		||||
	// Unmarshal the body into the user struct
 | 
			
		||||
	err = json.Unmarshal(body, &userInfo)
 | 
			
		||||
 | 
			
		||||
	// Check if there was an error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return user, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Get the user emails from github using the oauth http client
 | 
			
		||||
	res, err = client.Get("https://api.github.com/user/emails")
 | 
			
		||||
 | 
			
		||||
	// Check if there was an error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return user, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	defer res.Body.Close()
 | 
			
		||||
 | 
			
		||||
	log.Debug().Msg("Got email response from github")
 | 
			
		||||
 | 
			
		||||
	// Read the body of the response
 | 
			
		||||
	body, err = io.ReadAll(res.Body)
 | 
			
		||||
 | 
			
		||||
	// Check if there was an error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return user, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Debug().Msg("Read email body from github")
 | 
			
		||||
 | 
			
		||||
	// Parse the body into a user struct
 | 
			
		||||
	var emails GithubEmailResponse
 | 
			
		||||
 | 
			
		||||
	// Unmarshal the body into the user struct
 | 
			
		||||
	err = json.Unmarshal(body, &emails)
 | 
			
		||||
 | 
			
		||||
	// Check if there was an error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
		return user, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Debug().Msg("Parsed emails from github")
 | 
			
		||||
@@ -57,10 +102,28 @@ func GetGithubEmail(client *http.Client) (string, error) {
 | 
			
		||||
	// Find and return the primary email
 | 
			
		||||
	for _, email := range emails {
 | 
			
		||||
		if email.Primary {
 | 
			
		||||
			return email.Email, nil
 | 
			
		||||
			// Set the email then exit
 | 
			
		||||
			log.Debug().Str("email", email.Email).Msg("Found primary email")
 | 
			
		||||
			user.Email = email.Email
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// User does not have a primary email?
 | 
			
		||||
	return "", errors.New("no primary email found")
 | 
			
		||||
	// If no primary email was found, use the first available email
 | 
			
		||||
	if len(emails) == 0 {
 | 
			
		||||
		return user, errors.New("no emails found")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Set the email if it is not set picking the first one
 | 
			
		||||
	if user.Email == "" {
 | 
			
		||||
		log.Warn().Str("email", emails[0].Email).Msg("No primary email found, using first email")
 | 
			
		||||
		user.Email = emails[0].Email
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Set the username and name
 | 
			
		||||
	user.PreferredUsername = userInfo.Login
 | 
			
		||||
	user.Name = userInfo.Name
 | 
			
		||||
 | 
			
		||||
	// Return
 | 
			
		||||
	return user, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,29 +4,37 @@ import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"tinyauth/internal/constants"
 | 
			
		||||
 | 
			
		||||
	"github.com/rs/zerolog/log"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Google works the same as the generic provider
 | 
			
		||||
// Response for the google user endpoint
 | 
			
		||||
type GoogleUserInfoResponse struct {
 | 
			
		||||
	Email string `json:"email"`
 | 
			
		||||
	Name  string `json:"name"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// The scopes required for the google provider
 | 
			
		||||
func GoogleScopes() []string {
 | 
			
		||||
	return []string{"https://www.googleapis.com/auth/userinfo.email"}
 | 
			
		||||
	return []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetGoogleEmail(client *http.Client) (string, error) {
 | 
			
		||||
func GetGoogleUser(client *http.Client) (constants.Claims, error) {
 | 
			
		||||
	// Create user struct
 | 
			
		||||
	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")
 | 
			
		||||
 | 
			
		||||
	// Check if there was an error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
		return user, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	defer res.Body.Close()
 | 
			
		||||
 | 
			
		||||
	log.Debug().Msg("Got response from google")
 | 
			
		||||
 | 
			
		||||
	// Read the body of the response
 | 
			
		||||
@@ -34,24 +42,29 @@ func GetGoogleEmail(client *http.Client) (string, error) {
 | 
			
		||||
 | 
			
		||||
	// Check if there was an error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
		return user, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Debug().Msg("Read body from google")
 | 
			
		||||
 | 
			
		||||
	// Parse the body into a user struct
 | 
			
		||||
	var user GoogleUserInfoResponse
 | 
			
		||||
	// Create a new user info struct
 | 
			
		||||
	var userInfo GoogleUserInfoResponse
 | 
			
		||||
 | 
			
		||||
	// Unmarshal the body into the user struct
 | 
			
		||||
	err = json.Unmarshal(body, &user)
 | 
			
		||||
	err = json.Unmarshal(body, &userInfo)
 | 
			
		||||
 | 
			
		||||
	// Check if there was an error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
		return user, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Debug().Msg("Parsed user from google")
 | 
			
		||||
 | 
			
		||||
	// Return the email
 | 
			
		||||
	return user.Email, nil
 | 
			
		||||
	// Map the user info to the user struct
 | 
			
		||||
	user.PreferredUsername = strings.Split(userInfo.Email, "@")[0]
 | 
			
		||||
	user.Name = userInfo.Name
 | 
			
		||||
	user.Email = userInfo.Email
 | 
			
		||||
 | 
			
		||||
	// Return the user
 | 
			
		||||
	return user, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ package providers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"tinyauth/internal/constants"
 | 
			
		||||
	"tinyauth/internal/oauth"
 | 
			
		||||
	"tinyauth/internal/types"
 | 
			
		||||
 | 
			
		||||
@@ -93,14 +94,17 @@ func (providers *Providers) GetProvider(provider string) *oauth.OAuth {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (providers *Providers) GetUser(provider string) (string, error) {
 | 
			
		||||
	// Get the email from the provider
 | 
			
		||||
func (providers *Providers) GetUser(provider string) (constants.Claims, error) {
 | 
			
		||||
	// Create user struct
 | 
			
		||||
	var user constants.Claims
 | 
			
		||||
 | 
			
		||||
	// Get the user from the provider
 | 
			
		||||
	switch provider {
 | 
			
		||||
	case "github":
 | 
			
		||||
		// If the github provider is not configured, return an error
 | 
			
		||||
		if providers.Github == nil {
 | 
			
		||||
			log.Debug().Msg("Github provider not configured")
 | 
			
		||||
			return "", nil
 | 
			
		||||
			return user, nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Get the client from the github provider
 | 
			
		||||
@@ -108,23 +112,23 @@ func (providers *Providers) GetUser(provider string) (string, error) {
 | 
			
		||||
 | 
			
		||||
		log.Debug().Msg("Got client from github")
 | 
			
		||||
 | 
			
		||||
		// Get the email from the github provider
 | 
			
		||||
		email, err := GetGithubEmail(client)
 | 
			
		||||
		// Get the user from the github provider
 | 
			
		||||
		user, err := GetGithubUser(client)
 | 
			
		||||
 | 
			
		||||
		// Check if there was an error
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
			return user, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		log.Debug().Msg("Got email from github")
 | 
			
		||||
		log.Debug().Msg("Got user from github")
 | 
			
		||||
 | 
			
		||||
		// Return the email
 | 
			
		||||
		return email, nil
 | 
			
		||||
		// Return the user
 | 
			
		||||
		return user, nil
 | 
			
		||||
	case "google":
 | 
			
		||||
		// If the google provider is not configured, return an error
 | 
			
		||||
		if providers.Google == nil {
 | 
			
		||||
			log.Debug().Msg("Google provider not configured")
 | 
			
		||||
			return "", nil
 | 
			
		||||
			return user, nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Get the client from the google provider
 | 
			
		||||
@@ -132,23 +136,23 @@ func (providers *Providers) GetUser(provider string) (string, error) {
 | 
			
		||||
 | 
			
		||||
		log.Debug().Msg("Got client from google")
 | 
			
		||||
 | 
			
		||||
		// Get the email from the google provider
 | 
			
		||||
		email, err := GetGoogleEmail(client)
 | 
			
		||||
		// Get the user from the google provider
 | 
			
		||||
		user, err := GetGoogleUser(client)
 | 
			
		||||
 | 
			
		||||
		// Check if there was an error
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
			return user, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		log.Debug().Msg("Got email from google")
 | 
			
		||||
		log.Debug().Msg("Got user from google")
 | 
			
		||||
 | 
			
		||||
		// Return the email
 | 
			
		||||
		return email, nil
 | 
			
		||||
		// Return the user
 | 
			
		||||
		return user, nil
 | 
			
		||||
	case "generic":
 | 
			
		||||
		// If the generic provider is not configured, return an error
 | 
			
		||||
		if providers.Generic == nil {
 | 
			
		||||
			log.Debug().Msg("Generic provider not configured")
 | 
			
		||||
			return "", nil
 | 
			
		||||
			return user, nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Get the client from the generic provider
 | 
			
		||||
@@ -156,20 +160,20 @@ func (providers *Providers) GetUser(provider string) (string, error) {
 | 
			
		||||
 | 
			
		||||
		log.Debug().Msg("Got client from generic")
 | 
			
		||||
 | 
			
		||||
		// Get the email from the generic provider
 | 
			
		||||
		email, err := GetGenericEmail(client, providers.Config.GenericUserURL)
 | 
			
		||||
		// Get the user from the generic provider
 | 
			
		||||
		user, err := GetGenericUser(client, providers.Config.GenericUserURL)
 | 
			
		||||
 | 
			
		||||
		// Check if there was an error
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
			return user, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		log.Debug().Msg("Got email from generic")
 | 
			
		||||
		log.Debug().Msg("Got user from generic")
 | 
			
		||||
 | 
			
		||||
		// Return the email
 | 
			
		||||
		return email, nil
 | 
			
		||||
		return user, nil
 | 
			
		||||
	default:
 | 
			
		||||
		return "", nil
 | 
			
		||||
		return user, nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ type OAuthRequest struct {
 | 
			
		||||
type UnauthorizedQuery struct {
 | 
			
		||||
	Username string `url:"username"`
 | 
			
		||||
	Resource string `url:"resource"`
 | 
			
		||||
	GroupErr bool   `url:"groupErr"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Proxy is the uri parameters for the proxy endpoint
 | 
			
		||||
@@ -33,6 +34,8 @@ type UserContextResponse struct {
 | 
			
		||||
	Message     string `json:"message"`
 | 
			
		||||
	IsLoggedIn  bool   `json:"isLoggedIn"`
 | 
			
		||||
	Username    string `json:"username"`
 | 
			
		||||
	Name        string `json:"name"`
 | 
			
		||||
	Email       string `json:"email"`
 | 
			
		||||
	Provider    string `json:"provider"`
 | 
			
		||||
	Oauth       bool   `json:"oauth"`
 | 
			
		||||
	TotpPending bool   `json:"totpPending"`
 | 
			
		||||
@@ -48,6 +51,7 @@ type AppContext struct {
 | 
			
		||||
	GenericName           string   `json:"genericName"`
 | 
			
		||||
	Domain                string   `json:"domain"`
 | 
			
		||||
	ForgotPasswordMessage string   `json:"forgotPasswordMessage"`
 | 
			
		||||
	OAuthAutoRedirect     string   `json:"oauthAutoRedirect"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Totp request is the request for the totp endpoint
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,7 @@ type Config struct {
 | 
			
		||||
	GenericName             string `mapstructure:"generic-name"`
 | 
			
		||||
	DisableContinue         bool   `mapstructure:"disable-continue"`
 | 
			
		||||
	OAuthWhitelist          string `mapstructure:"oauth-whitelist"`
 | 
			
		||||
	OAuthAutoRedirect       string `mapstructure:"oauth-auto-redirect" validate:"oneof=none github google generic"`
 | 
			
		||||
	SessionExpiry           int    `mapstructure:"session-expiry"`
 | 
			
		||||
	LogLevel                int8   `mapstructure:"log-level" validate:"min=-1,max=5"`
 | 
			
		||||
	Title                   string `mapstructure:"app-title"`
 | 
			
		||||
@@ -44,6 +45,7 @@ type HandlersConfig struct {
 | 
			
		||||
	GenericName           string
 | 
			
		||||
	Title                 string
 | 
			
		||||
	ForgotPasswordMessage string
 | 
			
		||||
	OAuthAutoRedirect     string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// OAuthConfig is the configuration for the providers
 | 
			
		||||
@@ -78,3 +80,8 @@ type AuthConfig struct {
 | 
			
		||||
	LoginTimeout    int
 | 
			
		||||
	LoginMaxRetries int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HooksConfig is the configuration for the hooks service
 | 
			
		||||
type HooksConfig struct {
 | 
			
		||||
	Domain string
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -25,8 +25,11 @@ type OAuthProviders struct {
 | 
			
		||||
// SessionCookie is the cookie for the session (exculding the expiry)
 | 
			
		||||
type SessionCookie struct {
 | 
			
		||||
	Username    string
 | 
			
		||||
	Name        string
 | 
			
		||||
	Email       string
 | 
			
		||||
	Provider    string
 | 
			
		||||
	TotpPending bool
 | 
			
		||||
	OAuthGroups string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TinyauthLabels is the labels for the tinyauth container
 | 
			
		||||
@@ -35,15 +38,20 @@ type TinyauthLabels struct {
 | 
			
		||||
	Users          string
 | 
			
		||||
	Allowed        string
 | 
			
		||||
	Headers        map[string]string
 | 
			
		||||
	OAuthGroups    string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UserContext is the context for the user
 | 
			
		||||
type UserContext struct {
 | 
			
		||||
	Username    string
 | 
			
		||||
	Name        string
 | 
			
		||||
	Email       string
 | 
			
		||||
	IsLoggedIn  bool
 | 
			
		||||
	OAuth       bool
 | 
			
		||||
	Provider    string
 | 
			
		||||
	TotpPending bool
 | 
			
		||||
	OAuthGroups string
 | 
			
		||||
	TotpEnabled bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LoginAttempt tracks information about login attempts for rate limiting
 | 
			
		||||
 
 | 
			
		||||
@@ -204,6 +204,8 @@ func GetTinyauthLabels(labels map[string]string) types.TinyauthLabels {
 | 
			
		||||
					}
 | 
			
		||||
					tinyauthLabels.Headers[headerSplit[0]] = headerSplit[1]
 | 
			
		||||
				}
 | 
			
		||||
			case "tinyauth.oauth.groups":
 | 
			
		||||
				tinyauthLabels.OAuthGroups = value
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@@ -323,3 +325,22 @@ func CheckWhitelist(whitelist string, str string) bool {
 | 
			
		||||
	// Return false if no match was found
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Capitalize just the first letter of a string
 | 
			
		||||
func Capitalize(str string) string {
 | 
			
		||||
	if len(str) == 0 {
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
	return strings.ToUpper(string([]rune(str)[0])) + string([]rune(str)[1:])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Sanitize header removes all control characters from a string
 | 
			
		||||
func SanitizeHeader(header string) string {
 | 
			
		||||
	return strings.Map(func(r rune) rune {
 | 
			
		||||
		// Allow only printable ASCII characters (32-126) and safe whitespace (space, tab)
 | 
			
		||||
		if r == ' ' || r == '\t' || (r >= 32 && r <= 126) {
 | 
			
		||||
			return r
 | 
			
		||||
		}
 | 
			
		||||
		return -1
 | 
			
		||||
	}, header)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -467,3 +467,65 @@ func TestCheckWhitelist(t *testing.T) {
 | 
			
		||||
		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) {
 | 
			
		||||
	t.Log("Testing sanitize header with a valid string")
 | 
			
		||||
 | 
			
		||||
	// Create variables
 | 
			
		||||
	str := "X-Header=value"
 | 
			
		||||
	expected := "X-Header=value"
 | 
			
		||||
 | 
			
		||||
	// Test the sanitize header function
 | 
			
		||||
	result := utils.SanitizeHeader(str)
 | 
			
		||||
 | 
			
		||||
	// Check if the result is equal to the expected
 | 
			
		||||
	if result != expected {
 | 
			
		||||
		t.Fatalf("Expected %v, got %v", expected, result)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t.Log("Testing sanitize header with an invalid string")
 | 
			
		||||
 | 
			
		||||
	// Create variables
 | 
			
		||||
	str = "X-Header=val\nue"
 | 
			
		||||
	expected = "X-Header=value"
 | 
			
		||||
 | 
			
		||||
	// Test the sanitize header function
 | 
			
		||||
	result = utils.SanitizeHeader(str)
 | 
			
		||||
 | 
			
		||||
	// Check if the result is equal to the expected
 | 
			
		||||
	if result != expected {
 | 
			
		||||
		t.Fatalf("Expected %v, got %v", expected, result)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user