Compare commits

..

2 Commits

Author SHA1 Message Date
Stavros
df10eb65ce chore: tidy go mod 2025-04-03 15:45:03 +03:00
Stavros
8bf5a6067e wip 2025-04-03 15:44:47 +03:00
86 changed files with 1410 additions and 4223 deletions

View File

@@ -12,6 +12,9 @@ GITHUB_CLIENT_SECRET_FILE=github_client_secret_file
GOOGLE_CLIENT_ID=google_client_id GOOGLE_CLIENT_ID=google_client_id
GOOGLE_CLIENT_SECRET=google_client_secret GOOGLE_CLIENT_SECRET=google_client_secret
GOOGLE_CLIENT_SECRET_FILE=google_client_secret_file GOOGLE_CLIENT_SECRET_FILE=google_client_secret_file
TAILSCALE_CLIENT_ID=tailscale_client_id
TAILSCALE_CLIENT_SECRET=tailscale_client_secret
TAILSCALE_CLIENT_SECRET_FILE=tailscale__client_secret_file
GENERIC_CLIENT_ID=generic_client_id GENERIC_CLIENT_ID=generic_client_id
GENERIC_CLIENT_SECRET=generic_client_secret GENERIC_CLIENT_SECRET=generic_client_secret
GENERIC_CLIENT_SECRET_FILE=generic_client_secret_file GENERIC_CLIENT_SECRET_FILE=generic_client_secret_file
@@ -23,8 +26,5 @@ DISABLE_CONTINUE=false
OAUTH_WHITELIST= OAUTH_WHITELIST=
GENERIC_NAME=My OAuth GENERIC_NAME=My OAuth
SESSION_EXPIRY=7200 SESSION_EXPIRY=7200
LOGIN_TIMEOUT=300
LOGIN_MAX_RETRIES=5
LOG_LEVEL=0 LOG_LEVEL=0
APP_TITLE=Tinyauth SSO APP_TITLE=Tinyauth SSO
FORGOT_PASSWORD_MESSAGE=Some message about resetting the password

View File

@@ -1,14 +0,0 @@
version: 2
updates:
- package-ecosystem: "bun"
directory: "/frontend"
schedule:
interval: "daily"
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "daily"

View File

@@ -4,9 +4,11 @@ on:
branches: branches:
- main - main
pull_request: pull_request:
branches:
- main
jobs: jobs:
ci: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code

View File

@@ -6,85 +6,7 @@ on:
- "v*" - "v*"
jobs: jobs:
binary-build: build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions/setup-go@v5
with:
go-version: "^1.23.2"
- name: Install frontend dependencies
run: |
cd frontend
bun install
- name: Install backend dependencies
run: |
go mod tidy
- name: Build frontend
run: |
cd frontend
bun run build
- name: Build
run: |
cp -r frontend/dist internal/assets/dist
CGO_ENABLED=0 go build -ldflags "-s -w" -o tinyauth-amd64
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: tinyauth-amd64
path: tinyauth-amd64
binary-build-arm:
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions/setup-go@v5
with:
go-version: "^1.23.2"
- name: Install frontend dependencies
run: |
cd frontend
bun install
- name: Install backend dependencies
run: |
go mod tidy
- name: Build frontend
run: |
cd frontend
bun run build
- name: Build
run: |
cp -r frontend/dist internal/assets/dist
CGO_ENABLED=0 go build -ldflags "-s -w" -o tinyauth-arm64
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: tinyauth-arm64
path: tinyauth-arm64
image-build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@@ -129,7 +51,7 @@ jobs:
if-no-files-found: error if-no-files-found: error
retention-days: 1 retention-days: 1
image-build-arm: build-arm:
runs-on: ubuntu-24.04-arm runs-on: ubuntu-24.04-arm
steps: steps:
- name: Checkout - name: Checkout
@@ -174,11 +96,11 @@ jobs:
if-no-files-found: error if-no-files-found: error
retention-days: 1 retention-days: 1
image-merge: merge:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- image-build - build
- image-build-arm - build-arm
steps: steps:
- name: Download digests - name: Download digests
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
@@ -212,20 +134,3 @@ jobs:
run: | run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *) $(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *)
update-release:
runs-on: ubuntu-latest
needs:
- binary-build
- binary-build-arm
steps:
- uses: actions/download-artifact@v4
with:
pattern: tinyauth-*
path: binaries
merge-multiple: true
- name: Release
uses: softprops/action-gh-release@v2
with:
files: binaries/*

View File

@@ -1,20 +0,0 @@
name: Close stale issues and PRs
on:
schedule:
- cron: 0 10 * * *
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
days-before-stale: 30
stale-pr-message: This PR has been inactive for 30 days and will be marked as stale.
stale-issue-message: This issue has been inactive for 30 days and will be marked as stale.
close-issue-message: Closed for inactivity.
close-pr-message: Closed for inactivity.
stale-issue-label: stale
stale-pr-label: stale
exempt-issue-labels: pinned
exempt-pr-labels: pinned

View File

@@ -3,7 +3,7 @@ name: Publish translations
on: on:
push: push:
branches: branches:
- i18n_v* - main
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@@ -16,53 +16,7 @@ concurrency:
cancel-in-progress: false cancel-in-progress: false
jobs: jobs:
get-branches:
runs-on: ubuntu-latest
outputs:
i18n-branches: ${{ steps.get-branches.outputs.result }}
steps:
- name: Get branches
id: get-branches
uses: actions/github-script@v7
with:
script: |
const { data: repos } = await github.rest.repos.listBranches({
owner: context.repo.owner,
repo: context.repo.repo,
})
const i18nBranches = repos.filter((branch) => branch.name.startsWith("i18n_v"))
const i18nBranchNames = i18nBranches.map((branch) => branch.name)
return i18nBranchNames
get-translations:
needs: get-branches
runs-on: ubuntu-latest
strategy:
matrix:
branch: ${{ fromJson(needs.get-branches.outputs.i18n-branches) }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ matrix.branch }}
- name: Get translation version
id: get-version
run: |
branch=${{ matrix.branch }}
version=${branch#i18n_}
echo "version=$version" >> $GITHUB_OUTPUT
- name: Upload translations
uses: actions/upload-artifact@v4
with:
name: ${{ steps.get-version.outputs.version }}
path: frontend/src/lib/i18n/locales
build: build:
needs: get-translations
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@@ -71,14 +25,10 @@ jobs:
- name: Setup Pages - name: Setup Pages
uses: actions/configure-pages@v4 uses: actions/configure-pages@v4
- name: Prepare output directory - name: Move translations
run: | run: |
mkdir -p dist/i18n/ mkdir -p dist
mv frontend/src/lib/i18n/locales dist/i18n
- name: Download translations
uses: actions/download-artifact@v4
with:
path: dist/i18n/
- name: Upload artifact - name: Upload artifact
uses: actions/upload-pages-artifact@v3 uses: actions/upload-pages-artifact@v3

View File

@@ -1,5 +1,5 @@
# Site builder # Site builder
FROM oven/bun:1.2.10-alpine AS frontend-builder FROM oven/bun:1.1.45-alpine AS frontend-builder
WORKDIR /frontend WORKDIR /frontend
@@ -21,7 +21,7 @@ COPY ./frontend/postcss.config.cjs ./
RUN bun run build RUN bun run build
# Builder # Builder
FROM golang:1.24-alpine3.21 AS builder FROM golang:1.23-alpine3.21 AS builder
WORKDIR /tinyauth WORKDIR /tinyauth

View File

@@ -1,4 +1,4 @@
FROM golang:1.24-alpine3.21 FROM golang:1.23-alpine3.21
WORKDIR /tinyauth WORKDIR /tinyauth
@@ -12,6 +12,9 @@ COPY ./internal ./internal
COPY ./main.go ./ COPY ./main.go ./
COPY ./air.toml ./ COPY ./air.toml ./
RUN mkdir -p ./internal/assets/dist && \
echo "app running" > ./internal/assets/dist/index.html
RUN go install github.com/air-verse/air@v1.61.7 RUN go install github.com/air-verse/air@v1.61.7
EXPOSE 3000 EXPOSE 3000

View File

@@ -15,9 +15,7 @@
<br /> <br />
Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic provider to all of your docker apps. It is designed for traefik but it can be extended to work with all reverse proxies like caddy and nginx. Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic OAuth provider to all of your docker apps. It is made for traefik but it can be extended to work with all reverse proxies like caddy and nginx.
![Login](assets/screenshot.png)
> [!WARNING] > [!WARNING]
> Tinyauth is in active development and configuration may change often. Please make sure to carefully read the release notes before updating. > Tinyauth is in active development and configuration may change often. Please make sure to carefully read the release notes before updating.
@@ -31,11 +29,11 @@ I just made a Discord server for Tinyauth! It is not only for Tinyauth but gener
## Getting Started ## Getting Started
You can easily get started with tinyauth by following the guide in the [documentation](https://tinyauth.app/docs/getting-started.html). There is also an available [docker compose file](./docker-compose.example.yml) that has traefik, whoami and tinyauth to demonstrate its capabilities. You can easily get started with tinyauth by following the guide on the [documentation](https://tinyauth.app/docs/getting-started.html). There is also an available [docker compose file](./docker-compose.example.yml) that has traefik, nginx and tinyauth to demonstrate its capabilities.
## Documentation ## Documentation
You can find documentation and guides on all of the available configuration of tinyauth [here](https://tinyauth.app). You can find documentation and guides on all available configuration of tinyauth [here](https://tinyauth.app).
## Contributing ## Contributing
@@ -53,9 +51,9 @@ 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: Thanks a lot to the following people for providing me with more coffee:
| <div align="center"><img height="64" src="https://avatars.githubusercontent.com/u/47644445?v=4" alt="Nicolas"></div> | <div align="center"><img height="64" src="https://avatars.githubusercontent.com/u/4255748?v=4" alt="Erwin"></div> | <div align="center"><img height="64" src="https://avatars.githubusercontent.com/u/7935041?v=4" alt="SimpleHomelab"></div> | <div align="center"><img height="64" src="https://avatars.githubusercontent.com/u/30562276?v=4" alt="jmadden91"></div> | | <img height="64" src="https://avatars.githubusercontent.com/u/47644445?v=4" alt="Nicolas"> | <img height="64" src="https://avatars.githubusercontent.com/u/4255748?v=4" alt="Erwin"> |
| -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
| <div align="center"><a href="https://github.com/nicotsx">Nicolas</a></div> | <div align="center"><a href="https://github.com/erwinkramer">Erwin</a></div> | <div align="center"><a href="https://github.com/SimpleHomelab">SimpleHomelab</a></div> | <div align="center"><a href="https://github.com/jmadden91">jmadden91</a></div> | | <div align="center"><a href="https://github.com/nicotsx">Nicolas</a></div> | <div align="center"><a href="https://github.com/erwinkramer">Erwin</a></div> |
## Acknowledgements ## Acknowledgements
@@ -63,7 +61,3 @@ Credits for the logo of this app go to:
- **Freepik** for providing the police hat and badge. - **Freepik** for providing the police hat and badge.
- **Renee French** for the original gopher logo. - **Renee French** for the original gopher logo.
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=steveiliop56/tinyauth&type=Date)](https://www.star-history.com/#steveiliop56/tinyauth&Date)

View File

@@ -2,7 +2,6 @@ root = "/tinyauth"
tmp_dir = "tmp" tmp_dir = "tmp"
[build] [build]
pre_cmd = ["mkdir -p internal/assets/dist", "echo 'backend running' > internal/assets/dist/index.html"]
cmd = "go build -o ./tmp/tinyauth ." cmd = "go build -o ./tmp/tinyauth ."
bin = "tmp/tinyauth" bin = "tmp/tinyauth"
include_ext = ["go"] include_ext = ["go"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

View File

@@ -2,6 +2,7 @@ package cmd
import ( import (
"errors" "errors"
"fmt"
"os" "os"
"strings" "strings"
"time" "time"
@@ -42,6 +43,7 @@ var rootCmd = &cobra.Command{
config.GithubClientSecret = utils.GetSecret(config.GithubClientSecret, config.GithubClientSecretFile) config.GithubClientSecret = utils.GetSecret(config.GithubClientSecret, config.GithubClientSecretFile)
config.GoogleClientSecret = utils.GetSecret(config.GoogleClientSecret, config.GoogleClientSecretFile) config.GoogleClientSecret = utils.GetSecret(config.GoogleClientSecret, config.GoogleClientSecretFile)
config.GenericClientSecret = utils.GetSecret(config.GenericClientSecret, config.GenericClientSecretFile) config.GenericClientSecret = utils.GetSecret(config.GenericClientSecret, config.GenericClientSecretFile)
config.TailscaleClientSecret = utils.GetSecret(config.TailscaleClientSecret, config.TailscaleClientSecretFile)
// Validate config // Validate config
validator := validator.New() validator := validator.New()
@@ -61,6 +63,13 @@ var rootCmd = &cobra.Command{
HandleError(errors.New("no users or OAuth configured"), "No users or OAuth configured") HandleError(errors.New("no users or OAuth configured"), "No users or OAuth configured")
} }
// Create oauth whitelist
oauthWhitelist := utils.Filter(strings.Split(config.OAuthWhitelist, ","), func(val string) bool {
return val != ""
})
log.Debug().Msg("Parsed OAuth whitelist")
// Get domain // Get domain
log.Debug().Msg("Getting domain") log.Debug().Msg("Getting domain")
domain, err := utils.GetUpperDomain(config.AppURL) domain, err := utils.GetUpperDomain(config.AppURL)
@@ -69,28 +78,29 @@ var rootCmd = &cobra.Command{
// Create OAuth config // Create OAuth config
oauthConfig := types.OAuthConfig{ oauthConfig := types.OAuthConfig{
GithubClientId: config.GithubClientId, GithubClientId: config.GithubClientId,
GithubClientSecret: config.GithubClientSecret, GithubClientSecret: config.GithubClientSecret,
GoogleClientId: config.GoogleClientId, GoogleClientId: config.GoogleClientId,
GoogleClientSecret: config.GoogleClientSecret, GoogleClientSecret: config.GoogleClientSecret,
GenericClientId: config.GenericClientId, TailscaleClientId: config.TailscaleClientId,
GenericClientSecret: config.GenericClientSecret, TailscaleClientSecret: config.TailscaleClientSecret,
GenericScopes: strings.Split(config.GenericScopes, ","), GenericClientId: config.GenericClientId,
GenericAuthURL: config.GenericAuthURL, GenericClientSecret: config.GenericClientSecret,
GenericTokenURL: config.GenericTokenURL, GenericScopes: strings.Split(config.GenericScopes, ","),
GenericUserURL: config.GenericUserURL, GenericAuthURL: config.GenericAuthURL,
AppURL: config.AppURL, GenericTokenURL: config.GenericTokenURL,
GenericUserURL: config.GenericUserURL,
AppURL: config.AppURL,
} }
// Create handlers config // Create handlers config
handlersConfig := types.HandlersConfig{ serverConfig := types.HandlersConfig{
AppURL: config.AppURL, AppURL: config.AppURL,
DisableContinue: config.DisableContinue, Domain: fmt.Sprintf(".%s", domain),
Title: config.Title, CookieSecure: config.CookieSecure,
GenericName: config.GenericName, DisableContinue: config.DisableContinue,
CookieSecure: config.CookieSecure, Title: config.Title,
Domain: domain, GenericName: config.GenericName,
ForgotPasswordMessage: config.FogotPasswordMessage,
} }
// Create api config // Create api config
@@ -99,23 +109,6 @@ var rootCmd = &cobra.Command{
Address: config.Address, Address: config.Address,
} }
// Create auth config
authConfig := types.AuthConfig{
Users: users,
OauthWhitelist: config.OAuthWhitelist,
Secret: config.Secret,
CookieSecure: config.CookieSecure,
SessionExpiry: config.SessionExpiry,
Domain: domain,
LoginTimeout: config.LoginTimeout,
LoginMaxRetries: config.LoginMaxRetries,
}
// Create hooks config
hooksConfig := types.HooksConfig{
Domain: domain,
}
// Create docker service // Create docker service
docker := docker.NewDocker() docker := docker.NewDocker()
@@ -124,7 +117,14 @@ var rootCmd = &cobra.Command{
HandleError(err, "Failed to initialize docker") HandleError(err, "Failed to initialize docker")
// Create auth service // Create auth service
auth := auth.NewAuth(authConfig, docker) auth := auth.NewAuth(types.AuthConfig{
Domain: domain,
Secret: config.Secret,
SessionExpiry: config.SessionExpiry,
CookieSecure: config.CookieSecure,
Users: users,
OAuthWhitelist: oauthWhitelist,
}, docker)
// Create OAuth providers service // Create OAuth providers service
providers := providers.NewProviders(oauthConfig) providers := providers.NewProviders(oauthConfig)
@@ -133,10 +133,10 @@ var rootCmd = &cobra.Command{
providers.Init() providers.Init()
// Create hooks service // Create hooks service
hooks := hooks.NewHooks(hooksConfig, auth, providers) hooks := hooks.NewHooks(auth, providers)
// Create handlers // Create handlers
handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker) handlers := handlers.NewHandlers(serverConfig, auth, hooks, providers, docker)
// Create API // Create API
api := api.NewAPI(apiConfig, handlers) api := api.NewAPI(apiConfig, handlers)
@@ -187,6 +187,9 @@ func init() {
rootCmd.Flags().String("google-client-id", "", "Google OAuth client ID.") rootCmd.Flags().String("google-client-id", "", "Google OAuth client ID.")
rootCmd.Flags().String("google-client-secret", "", "Google OAuth client secret.") rootCmd.Flags().String("google-client-secret", "", "Google OAuth client secret.")
rootCmd.Flags().String("google-client-secret-file", "", "Google OAuth client secret file.") rootCmd.Flags().String("google-client-secret-file", "", "Google OAuth client secret file.")
rootCmd.Flags().String("tailscale-client-id", "", "Tailscale OAuth client ID.")
rootCmd.Flags().String("tailscale-client-secret", "", "Tailscale OAuth client secret.")
rootCmd.Flags().String("tailscale-client-secret-file", "", "Tailscale OAuth client secret file.")
rootCmd.Flags().String("generic-client-id", "", "Generic OAuth client ID.") rootCmd.Flags().String("generic-client-id", "", "Generic OAuth client ID.")
rootCmd.Flags().String("generic-client-secret", "", "Generic OAuth client secret.") rootCmd.Flags().String("generic-client-secret", "", "Generic OAuth client secret.")
rootCmd.Flags().String("generic-client-secret-file", "", "Generic OAuth client secret file.") rootCmd.Flags().String("generic-client-secret-file", "", "Generic OAuth client secret file.")
@@ -198,11 +201,8 @@ func init() {
rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.") rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.")
rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.") rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.")
rootCmd.Flags().Int("session-expiry", 86400, "Session (cookie) expiration time in seconds.") rootCmd.Flags().Int("session-expiry", 86400, "Session (cookie) expiration time in seconds.")
rootCmd.Flags().Int("login-timeout", 300, "Login timeout in seconds after max retries reached (0 to disable).")
rootCmd.Flags().Int("login-max-retries", 5, "Maximum login attempts before timeout (0 to disable).")
rootCmd.Flags().Int("log-level", 1, "Log level.") rootCmd.Flags().Int("log-level", 1, "Log level.")
rootCmd.Flags().String("app-title", "Tinyauth", "Title of the app.") rootCmd.Flags().String("app-title", "Tinyauth", "Title of the app.")
rootCmd.Flags().String("forgot-password-message", "You can reset your password by changing the `USERS` environment variable.", "Message to show on the forgot password page.")
// Bind flags to environment // Bind flags to environment
viper.BindEnv("port", "PORT") viper.BindEnv("port", "PORT")
@@ -219,6 +219,9 @@ func init() {
viper.BindEnv("google-client-id", "GOOGLE_CLIENT_ID") viper.BindEnv("google-client-id", "GOOGLE_CLIENT_ID")
viper.BindEnv("google-client-secret", "GOOGLE_CLIENT_SECRET") viper.BindEnv("google-client-secret", "GOOGLE_CLIENT_SECRET")
viper.BindEnv("google-client-secret-file", "GOOGLE_CLIENT_SECRET_FILE") viper.BindEnv("google-client-secret-file", "GOOGLE_CLIENT_SECRET_FILE")
viper.BindEnv("tailscale-client-id", "TAILSCALE_CLIENT_ID")
viper.BindEnv("tailscale-client-secret", "TAILSCALE_CLIENT_SECRET")
viper.BindEnv("tailscale-client-secret-file", "TAILSCALE_CLIENT_SECRET_FILE")
viper.BindEnv("generic-client-id", "GENERIC_CLIENT_ID") viper.BindEnv("generic-client-id", "GENERIC_CLIENT_ID")
viper.BindEnv("generic-client-secret", "GENERIC_CLIENT_SECRET") viper.BindEnv("generic-client-secret", "GENERIC_CLIENT_SECRET")
viper.BindEnv("generic-client-secret-file", "GENERIC_CLIENT_SECRET_FILE") viper.BindEnv("generic-client-secret-file", "GENERIC_CLIENT_SECRET_FILE")
@@ -232,9 +235,6 @@ func init() {
viper.BindEnv("session-expiry", "SESSION_EXPIRY") viper.BindEnv("session-expiry", "SESSION_EXPIRY")
viper.BindEnv("log-level", "LOG_LEVEL") viper.BindEnv("log-level", "LOG_LEVEL")
viper.BindEnv("app-title", "APP_TITLE") viper.BindEnv("app-title", "APP_TITLE")
viper.BindEnv("login-timeout", "LOGIN_TIMEOUT")
viper.BindEnv("login-max-retries", "LOGIN_MAX_RETRIES")
viper.BindEnv("forgot-password-message", "FORGOT_PASSWORD_MESSAGE")
// Bind flags to viper // Bind flags to viper
viper.BindPFlags(rootCmd.Flags()) viper.BindPFlags(rootCmd.Flags())

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -14,34 +14,33 @@
"@mantine/form": "^7.16.0", "@mantine/form": "^7.16.0",
"@mantine/hooks": "^7.16.0", "@mantine/hooks": "^7.16.0",
"@mantine/notifications": "^7.16.0", "@mantine/notifications": "^7.16.0",
"@tanstack/react-query": "5", "@tanstack/react-query": "4",
"axios": "^1.7.9", "axios": "^1.7.9",
"i18next": "^25.0.0", "i18next": "^24.2.3",
"i18next-browser-languagedetector": "^8.0.4", "i18next-browser-languagedetector": "^8.0.4",
"i18next-chained-backend": "^4.6.2", "i18next-chained-backend": "^4.6.2",
"i18next-http-backend": "^3.0.2", "i18next-http-backend": "^3.0.2",
"i18next-resources-to-backend": "^1.2.1", "i18next-resources-to-backend": "^1.2.1",
"react": "^19.1.0", "react": "^18.3.1",
"react-dom": "^19.1.0", "react-dom": "^18.3.1",
"react-i18next": "^15.4.1", "react-i18next": "^15.4.1",
"react-markdown": "^10.1.0", "react-router": "^7.1.3",
"react-router": "^7.5.2",
"zod": "^3.24.1" "zod": "^3.24.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.17.0", "@eslint/js": "^9.17.0",
"@types/react": "^19.1.1", "@types/react": "^18.3.18",
"@types/react-dom": "^19.1.2", "@types/react-dom": "^18.3.5",
"@vitejs/plugin-react-swc": "^3.5.0", "@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^9.17.0", "eslint": "^9.17.0",
"eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16", "eslint-plugin-react-refresh": "^0.4.16",
"globals": "^16.0.0", "globals": "^15.14.0",
"postcss": "^8.5.1", "postcss": "^8.5.1",
"postcss-preset-mantine": "^1.17.0", "postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
"prettier": "3.5.3", "prettier": "3.4.2",
"typescript": "~5.8.3", "typescript": "~5.6.2",
"typescript-eslint": "^8.18.2", "typescript-eslint": "^8.18.2",
"vite": "^6.0.5" "vite": "^6.0.5"
} }

View File

@@ -1,15 +1,15 @@
import { TextInput, PasswordInput, Button, Anchor, Group, Text } from "@mantine/core"; import { TextInput, PasswordInput, Button } from "@mantine/core";
import { useForm, zodResolver } from "@mantine/form"; import { useForm, zodResolver } from "@mantine/form";
import { LoginFormValues, loginSchema } from "../../schemas/login-schema"; import { LoginFormValues, loginSchema } from "../../schemas/login-schema";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
interface LoginFormProps { interface LoginFormProps {
isPending: boolean; isLoading: boolean;
onSubmit: (values: LoginFormValues) => void; onSubmit: (values: LoginFormValues) => void;
} }
export const LoginForm = (props: LoginFormProps) => { export const LoginForm = (props: LoginFormProps) => {
const { isPending, onSubmit } = props; const { isLoading, onSubmit } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const form = useForm({ const form = useForm({
@@ -25,31 +25,22 @@ export const LoginForm = (props: LoginFormProps) => {
<form onSubmit={form.onSubmit(onSubmit)}> <form onSubmit={form.onSubmit(onSubmit)}>
<TextInput <TextInput
label={t("loginUsername")} label={t("loginUsername")}
placeholder="Username" placeholder="user@example.com"
disabled={isPending}
required required
withAsterisk={false} disabled={isLoading}
key={form.key("username")} key={form.key("username")}
{...form.getInputProps("username")} {...form.getInputProps("username")}
/> />
<Group justify="space-between" mb={5} mt="md">
<Text component="label" htmlFor=".password-input" size="sm" fw={500}>
{t("loginPassword")}
</Text>
<Anchor href="#" onClick={() => window.location.replace("/forgot-password")} pt={2} fw={500} fz="xs">
{t('forgotPasswordTitle')}
</Anchor>
</Group>
<PasswordInput <PasswordInput
className="password-input" label={t("loginPassword")}
placeholder="Password" placeholder="password"
required required
disabled={isPending} mt="md"
disabled={isLoading}
key={form.key("password")} key={form.key("password")}
{...form.getInputProps("password")} {...form.getInputProps("password")}
/> />
<Button fullWidth mt="xl" type="submit" loading={isPending}> <Button fullWidth mt="xl" type="submit" loading={isLoading}>
{t("loginSubmit")} {t("loginSubmit")}
</Button> </Button>
</form> </form>

View File

@@ -2,16 +2,17 @@ import { Grid, Button } from "@mantine/core";
import { GithubIcon } from "../../icons/github"; import { GithubIcon } from "../../icons/github";
import { GoogleIcon } from "../../icons/google"; import { GoogleIcon } from "../../icons/google";
import { OAuthIcon } from "../../icons/oauth"; import { OAuthIcon } from "../../icons/oauth";
import { TailscaleIcon } from "../../icons/tailscale";
interface OAuthButtonsProps { interface OAuthButtonsProps {
oauthProviders: string[]; oauthProviders: string[];
isPending: boolean; isLoading: boolean;
mutate: (provider: string) => void; mutate: (provider: string) => void;
genericName: string; genericName: string;
} }
export const OAuthButtons = (props: OAuthButtonsProps) => { export const OAuthButtons = (props: OAuthButtonsProps) => {
const { oauthProviders, isPending, genericName, mutate } = props; const { oauthProviders, isLoading, genericName, mutate } = props;
return ( return (
<Grid mb="md" mt="md" align="center" justify="center"> <Grid mb="md" mt="md" align="center" justify="center">
{oauthProviders.includes("google") && ( {oauthProviders.includes("google") && (
@@ -21,7 +22,7 @@ export const OAuthButtons = (props: OAuthButtonsProps) => {
leftSection={<GoogleIcon style={{ width: 14, height: 14 }} />} leftSection={<GoogleIcon style={{ width: 14, height: 14 }} />}
variant="default" variant="default"
onClick={() => mutate("google")} onClick={() => mutate("google")}
loading={isPending} loading={isLoading}
> >
Google Google
</Button> </Button>
@@ -34,12 +35,25 @@ export const OAuthButtons = (props: OAuthButtonsProps) => {
leftSection={<GithubIcon style={{ width: 14, height: 14 }} />} leftSection={<GithubIcon style={{ width: 14, height: 14 }} />}
variant="default" variant="default"
onClick={() => mutate("github")} onClick={() => mutate("github")}
loading={isPending} loading={isLoading}
> >
Github Github
</Button> </Button>
</Grid.Col> </Grid.Col>
)} )}
{oauthProviders.includes("tailscale") && (
<Grid.Col span="content">
<Button
radius="xl"
leftSection={<TailscaleIcon style={{ width: 14, height: 14 }} />}
variant="default"
onClick={() => mutate("tailscale")}
loading={isLoading}
>
Tailscale
</Button>
</Grid.Col>
)}
{oauthProviders.includes("generic") && ( {oauthProviders.includes("generic") && (
<Grid.Col span="content"> <Grid.Col span="content">
<Button <Button
@@ -47,7 +61,7 @@ export const OAuthButtons = (props: OAuthButtonsProps) => {
leftSection={<OAuthIcon style={{ width: 14, height: 14 }} />} leftSection={<OAuthIcon style={{ width: 14, height: 14 }} />}
variant="default" variant="default"
onClick={() => mutate("generic")} onClick={() => mutate("generic")}
loading={isPending} loading={isLoading}
> >
{genericName} {genericName}
</Button> </Button>

View File

@@ -10,11 +10,11 @@ type FormValues = z.infer<typeof schema>;
interface TotpFormProps { interface TotpFormProps {
onSubmit: (values: FormValues) => void; onSubmit: (values: FormValues) => void;
isPending: boolean; isLoading: boolean;
} }
export const TotpForm = (props: TotpFormProps) => { export const TotpForm = (props: TotpFormProps) => {
const { onSubmit, isPending } = props; const { onSubmit, isLoading } = props;
const form = useForm({ const form = useForm({
mode: "uncontrolled", mode: "uncontrolled",
@@ -32,7 +32,7 @@ export const TotpForm = (props: TotpFormProps) => {
placeholder="" placeholder=""
{...form.getInputProps("code")} {...form.getInputProps("code")}
/> />
<Button type="submit" mt="xl" loading={isPending} fullWidth> <Button type="submit" mt="xl" loading={isLoading} fullWidth>
Verify Verify
</Button> </Button>
</form> </form>

View File

@@ -1,4 +1,4 @@
import { useSuspenseQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import React, { createContext, useContext } from "react"; import React, { createContext, useContext } from "react";
import axios from "axios"; import axios from "axios";
import { AppContextSchemaType } from "../schemas/app-context-schema"; import { AppContextSchemaType } from "../schemas/app-context-schema";
@@ -14,7 +14,7 @@ export const AppContextProvider = ({
data: userContext, data: userContext,
isLoading, isLoading,
error, error,
} = useSuspenseQuery({ } = useQuery({
queryKey: ["appContext"], queryKey: ["appContext"],
queryFn: async () => { queryFn: async () => {
const res = await axios.get("/api/app"); const res = await axios.get("/api/app");

View File

@@ -1,4 +1,4 @@
import { useSuspenseQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import React, { createContext, useContext } from "react"; import React, { createContext, useContext } from "react";
import axios from "axios"; import axios from "axios";
import { UserContextSchemaType } from "../schemas/user-context-schema"; import { UserContextSchemaType } from "../schemas/user-context-schema";
@@ -14,7 +14,7 @@ export const UserContextProvider = ({
data: userContext, data: userContext,
isLoading, isLoading,
error, error,
} = useSuspenseQuery({ } = useQuery({
queryKey: ["userContext"], queryKey: ["userContext"],
queryFn: async () => { queryFn: async () => {
const res = await axios.get("/api/user"); const res = await axios.get("/api/user");

View File

@@ -0,0 +1,55 @@
import type { SVGProps } from "react";
export function TailscaleIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
width={24}
height={24}
{...props}
>
<style>{".st0{opacity:0.2;fill:#CCCAC9;}.st1{fill:#FFFFFF;}"}</style>
<g>
<g>
<path
className="st0"
d="M65.6,127.7c35.3,0,63.9-28.6,63.9-63.9S100.9,0,65.6,0S1.8,28.6,1.8,63.9S30.4,127.7,65.6,127.7z"
/>
<path
className="st1"
d="M65.6,318.1c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9S1.8,219,1.8,254.2S30.4,318.1,65.6,318.1z"
/>
<path
className="st0"
d="M65.6,512c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9S1.8,412.9,1.8,448.1S30.4,512,65.6,512z"
/>
<path
className="st1"
d="M257.2,318.1c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9s-63.9,28.6-63.9,63.9S221.9,318.1,257.2,318.1z"
/>
<path
className="st1"
d="M257.2,512c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9s-63.9,28.6-63.9,63.9S221.9,512,257.2,512z"
/>
<path
className="st0"
d="M257.2,127.7c35.3,0,63.9-28.6,63.9-63.9S292.5,0,257.2,0s-63.9,28.6-63.9,63.9S221.9,127.7,257.2,127.7z"
/>
<path
className="st0"
d="M446.4,127.7c35.3,0,63.9-28.6,63.9-63.9S481.6,0,446.4,0c-35.3,0-63.9,28.6-63.9,63.9S411.1,127.7,446.4,127.7z"
/>
<path
className="st1"
d="M446.4,318.1c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9s-63.9,28.6-63.9,63.9S411.1,318.1,446.4,318.1z"
/>
<path
className="st0"
d="M446.4,512c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9s-63.9,28.6-63.9,63.9S411.1,512,446.4,512z"
/>
</g>
</g>
</svg>
);
}

View File

@@ -1,4 +0,0 @@
span,
p {
word-break: break-word;
}

View File

@@ -5,20 +5,6 @@ import ChainedBackend from "i18next-chained-backend";
import resourcesToBackend from "i18next-resources-to-backend"; import resourcesToBackend from "i18next-resources-to-backend";
import HttpBackend from "i18next-http-backend"; import HttpBackend from "i18next-http-backend";
const backends = [
HttpBackend,
resourcesToBackend(
(language: string) => import(`./locales/${language}.json`),
),
]
const backendOptions = [
{
loadPath: "https://cdn.tinyauth.app/i18n/v1/{{lng}}.json",
},
{}
]
i18n i18n
.use(ChainedBackend) .use(ChainedBackend)
.use(LanguageDetector) .use(LanguageDetector)
@@ -34,8 +20,17 @@ i18n
load: "currentOnly", load: "currentOnly",
backend: { backend: {
backends: import.meta.env.MODE !== "development" ? backends : backends.reverse(), backends: [
backendOptions: import.meta.env.MODE !== "development" ? backendOptions : backendOptions.reverse() HttpBackend,
resourcesToBackend(
(language: string) => import(`./locales/${language}.json`),
),
],
backendOptions: [
{
loadPath: "https://cdn.tinyauth.app/i18n/{{lng}}.json",
},
],
}, },
}); });

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,10 +39,7 @@
"totpSuccessSubtitle": "Redirecting to your app", "totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code", "totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized", "unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again", "unauthorizedButton": "Try again"
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
"cancelTitle": "Cancel"
} }

View File

@@ -1,49 +1,45 @@
{ {
"loginTitle": "مرحبا بعودتك، قم بتسجيل الدخول باستخدام", "loginTitle": "Welcome back, login with",
"loginDivider": "أو المتابعة بكلمة المرور", "loginDivider": "Or continue with password",
"loginUsername": "اسم المستخدم", "loginUsername": "Username",
"loginPassword": "كلمة المرور", "loginPassword": "Password",
"loginSubmit": "تسجيل الدخول", "loginSubmit": "Login",
"loginFailTitle": "فشل تسجيل الدخول", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "الرجاء التحقق من اسم المستخدم وكلمة المرور", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "فشلت في تسجيل الدخول عدة مرات، الرجاء المحاولة مرة أخرى لاحقا", "loginSuccessTitle": "Logged in",
"loginSuccessTitle": "تم تسجيل الدخول", "loginSuccessSubtitle": "Welcome back!",
"loginSuccessSubtitle": "مرحبا بعودتك!", "loginOauthFailTitle": "Internal error",
"loginOauthFailTitle": "خطأ داخلي", "loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthFailSubtitle": "فشل في الحصول على رابط OAuth", "loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessTitle": "إعادة توجيه", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthSuccessSubtitle": "إعادة توجيه إلى مزود OAuth الخاص بك", "continueRedirectingTitle": "Redirecting...",
"continueRedirectingTitle": "إعادة توجيه...", "continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectingSubtitle": "يجب إعادة توجيهك إلى التطبيق قريبا", "continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectTitle": "إعادة توجيه غير صالحة", "continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInvalidRedirectSubtitle": "رابط إعادة التوجيه غير صالح", "continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectTitle": "إعادة توجيه غير آمنة", "continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
"continueInsecureRedirectSubtitle": "أنت تحاول إعادة التوجيه من <Code>https</Code> إلى <Code>http</Code>، هل أنت متأكد أنك تريد المتابعة؟", "continueTitle": "Continue",
"continueTitle": "متابعة", "continueSubtitle": "Click the button to continue to your app.",
"continueSubtitle": "انقر الزر للمتابعة إلى التطبيق الخاص بك.", "internalErrorTitle": "Internal Server Error",
"internalErrorTitle": "خطأ داخلي في الخادم", "internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
"internalErrorSubtitle": "حدث خطأ على الخادم ولا يمكن حاليا تلبية طلبك.", "internalErrorButton": "Try again",
"internalErrorButton": "حاول مجددا", "logoutFailTitle": "Failed to log out",
"logoutFailTitle": "فشل تسجيل الخروج", "logoutFailSubtitle": "Please try again",
"logoutFailSubtitle": "يرجى إعادة المحاولة", "logoutSuccessTitle": "Logged out",
"logoutSuccessTitle": "تم تسجيل الخروج", "logoutSuccessSubtitle": "You have been logged out",
"logoutSuccessSubtitle": "تم تسجيل خروجك", "logoutTitle": "Logout",
"logoutTitle": "تسجيل الخروج", "logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
"logoutUsernameSubtitle": "أنت حاليا مسجل الدخول ك <Code>{{username}}</Code>، انقر الزر أدناه لتسجيل الخروج.", "logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
"logoutOauthSubtitle": "أنت حاليا مسجل الدخول ك <Code>{{username}}</Code> باستخدام مزود OAuth {{provider}} ، انقر الزر أدناه لتسجيل الخروج.", "notFoundTitle": "Page not found",
"notFoundTitle": "الصفحة غير موجودة", "notFoundSubtitle": "The page you are looking for does not exist.",
"notFoundSubtitle": "الصفحة التي تبحث عنها غير موجودة.", "notFoundButton": "Go home",
"notFoundButton": "انتقل إلى الرئيسية", "totpFailTitle": "Failed to verify code",
"totpFailTitle": "فشل في التحقق من الرمز", "totpFailSubtitle": "Please check your code and try again",
"totpFailSubtitle": "الرجاء التحقق من الرمز الخاص بك وحاول مرة أخرى", "totpSuccessTitle": "Verified",
"totpSuccessTitle": "تم التحقق", "totpSuccessSubtitle": "Redirecting to your app",
"totpSuccessSubtitle": "إعادة توجيه إلى تطبيقك", "totpTitle": "Enter your TOTP code",
"totpTitle": "أدخل رمز TOTP الخاص بك", "unauthorizedTitle": "Unauthorized",
"unauthorizedTitle": "غير مرخص", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unauthorizedResourceSubtitle": "المستخدم الذي يحمل اسم المستخدم <Code>{{username}}</Code> غير مصرح له بالوصول إلى المورد <Code>{{resource}}</Code>.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unaothorizedLoginSubtitle": "المستخدم الذي يحمل اسم المستخدم <Code>{{username}}</Code> غير مصرح له بتسجيل الدخول.", "unauthorizedButton": "Try again"
"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": "إلغاء"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,10 +39,7 @@
"totpSuccessSubtitle": "Redirecting to your app", "totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code", "totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized", "unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again", "unauthorizedButton": "Try again"
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
"cancelTitle": "Cancel"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,10 +39,7 @@
"totpSuccessSubtitle": "Redirecting to your app", "totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code", "totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized", "unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again", "unauthorizedButton": "Try again"
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
"cancelTitle": "Cancel"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,10 +39,7 @@
"totpSuccessSubtitle": "Redirecting to your app", "totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code", "totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized", "unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again", "unauthorizedButton": "Try again"
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
"cancelTitle": "Cancel"
} }

View File

@@ -1,49 +1,45 @@
{ {
"loginTitle": "Willkommen zurück, logge dich ein mit", "loginTitle": "Welcome back, login with",
"loginDivider": "Oder mit Passwort fortfahren", "loginDivider": "Or continue with password",
"loginUsername": "Benutzername", "loginUsername": "Username",
"loginPassword": "Passwort", "loginPassword": "Password",
"loginSubmit": "Anmelden", "loginSubmit": "Login",
"loginFailTitle": "Login fehlgeschlagen", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Bitte überprüfe deinen Benutzernamen und Passwort", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "Sie konnten sich zu oft nicht einloggen, bitte versuchen Sie es später erneut", "loginSuccessTitle": "Logged in",
"loginSuccessTitle": "Angemeldet", "loginSuccessSubtitle": "Welcome back!",
"loginSuccessSubtitle": "Willkommen zurück!", "loginOauthFailTitle": "Internal error",
"loginOauthFailTitle": "Interner Fehler", "loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthFailSubtitle": "Fehler beim Abrufen der OAuth-URL", "loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessTitle": "Leite weiter", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthSuccessSubtitle": "Weiterleitung zu Ihrem OAuth-Provider", "continueRedirectingTitle": "Redirecting...",
"continueRedirectingTitle": "Leite weiter...", "continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectingSubtitle": "Sie sollten in Kürze zur App weitergeleitet werden", "continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectTitle": "Ungültige Weiterleitung", "continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInvalidRedirectSubtitle": "Die Weiterleitungs-URL ist ungültig", "continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectTitle": "Unsichere Weiterleitung", "continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
"continueInsecureRedirectSubtitle": "Sie versuchen von <Code>https</Code> auf <Code>http</Code>weiterzuleiten. Sind Sie sicher, dass Sie fortfahren möchten?", "continueTitle": "Continue",
"continueTitle": "Weiter", "continueSubtitle": "Click the button to continue to your app.",
"continueSubtitle": "Klicken Sie auf den Button, um zur App zu gelangen.", "internalErrorTitle": "Internal Server Error",
"internalErrorTitle": "Interner Serverfehler", "internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
"internalErrorSubtitle": "Ein Error ist auf dem Server aufgetreten, weshalb ihre Anfrage derzeit nicht bearbeitet werden kann.", "internalErrorButton": "Try again",
"internalErrorButton": "Erneut versuchen", "logoutFailTitle": "Failed to log out",
"logoutFailTitle": "Abmelden fehlgeschlagen", "logoutFailSubtitle": "Please try again",
"logoutFailSubtitle": "Bitte versuchen Sie es erneut", "logoutSuccessTitle": "Logged out",
"logoutSuccessTitle": "Abgemeldet", "logoutSuccessSubtitle": "You have been logged out",
"logoutSuccessSubtitle": "Sie wurden abgemeldet", "logoutTitle": "Logout",
"logoutTitle": "Abmelden", "logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
"logoutUsernameSubtitle": "Sie sind derzeit als <Code>{{username}}</Code>angemeldet. Klicken Sie auf den Button unten, um sich abzumelden.", "logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
"logoutOauthSubtitle": "Sie sind derzeit als <Code>{{username}}</Code> mit dem {{provider}} OAuth-Anbieter angemeldet. Klicken Sie auf den Button unten, um sich abzumelden.", "notFoundTitle": "Page not found",
"notFoundTitle": "Seite nicht gefunden", "notFoundSubtitle": "The page you are looking for does not exist.",
"notFoundSubtitle": "Die gesuchte Seite existiert nicht.", "notFoundButton": "Go home",
"notFoundButton": "Nach Hause", "totpFailTitle": "Failed to verify code",
"totpFailTitle": "Fehler beim Verifizieren des Codes", "totpFailSubtitle": "Please check your code and try again",
"totpFailSubtitle": "Bitte überprüfen Sie Ihren Code und versuchen Sie es erneut", "totpSuccessTitle": "Verified",
"totpSuccessTitle": "Verifiziert", "totpSuccessSubtitle": "Redirecting to your app",
"totpSuccessSubtitle": "Leite zur App weiter", "totpTitle": "Enter your TOTP code",
"totpTitle": "Geben Sie Ihren TOTP Code ein", "unauthorizedTitle": "Unauthorized",
"unauthorizedTitle": "Unautorisiert", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unauthorizedResourceSubtitle": "Der Benutzer mit Benutzername <Code>{{username}}</Code> ist nicht berechtigt auf die Ressource <Code>{{resource}}</Code> zuzugreifen.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unaothorizedLoginSubtitle": "Der Benutzer mit dem Benutzernamen <Code>{{username}}</Code> ist nicht berechtigt, sich einzuloggen.", "unauthorizedButton": "Try again"
"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"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Είσοδος", "loginSubmit": "Είσοδος",
"loginFailTitle": "Αποτυχία σύνδεσης", "loginFailTitle": "Αποτυχία σύνδεσης",
"loginFailSubtitle": "Παρακαλώ ελέγξτε το όνομα χρήστη και τον κωδικό πρόσβασης", "loginFailSubtitle": "Παρακαλώ ελέγξτε το όνομα χρήστη και τον κωδικό πρόσβασης",
"loginFailRateLimit": "Αποτύχατε να συνδεθείτε πάρα πολλές φορές, παρακαλώ προσπαθήστε ξανά αργότερα",
"loginSuccessTitle": "Συνδεδεμένος", "loginSuccessTitle": "Συνδεδεμένος",
"loginSuccessSubtitle": "Καλώς ήρθατε!", "loginSuccessSubtitle": "Καλώς ήρθατε!",
"loginOauthFailTitle": "Εσωτερικό σφάλμα", "loginOauthFailTitle": "Εσωτερικό σφάλμα",
@@ -40,10 +39,7 @@
"totpSuccessSubtitle": "Ανακατεύθυνση στην εφαρμογή σας", "totpSuccessSubtitle": "Ανακατεύθυνση στην εφαρμογή σας",
"totpTitle": "Εισάγετε τον κωδικό TOTP", "totpTitle": "Εισάγετε τον κωδικό TOTP",
"unauthorizedTitle": "Μη εξουσιοδοτημένο", "unauthorizedTitle": "Μη εξουσιοδοτημένο",
"unauthorizedResourceSubtitle": "Ο χρήστης με όνομα χρήστη <Code>{{username}}</Code> δεν έχει άδεια πρόσβασης στον πόρο <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "Ο χρήστης με όνομα χρήστη {{username}} δεν είναι εξουσιοδοτημένος να έχει πρόσβαση στον πόρο <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "Ο χρήστης με όνομα χρήστη <Code>{{username}}</Code> δεν είναι εξουσιοδοτημένος να συνδεθεί.", "unaothorizedLoginSubtitle": "Ο χρήστης με όνομα χρήστη {{username}} δεν είναι εξουσιοδοτημένος να συνδεθεί.",
"unauthorizedButton": "Προσπαθήστε ξανά", "unauthorizedButton": "Προσπαθήστε ξανά"
"untrustedRedirectTitle": "Μη έμπιστη ανακατεύθυνση",
"untrustedRedirectSubtitle": "Προσπαθείτε να ανακατευθύνετε σε έναν τομέα που δεν ταιριάζει με τον ρυθμισμένο τομέα σας (<Code>{{domain}}</Code>). Είστε βέβαιοι ότι θέλετε να συνεχίσετε;",
"cancelTitle": "Ακύρωση"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,12 +39,7 @@
"totpSuccessSubtitle": "Redirecting to your app", "totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code", "totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized", "unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} 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"
"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",
"forgotPasswordTitle": "Forgot your password?"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,12 +39,7 @@
"totpSuccessSubtitle": "Redirecting to your app", "totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code", "totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized", "unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} 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"
"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",
"forgotPasswordTitle": "Forgot your password?"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,10 +39,7 @@
"totpSuccessSubtitle": "Redirecting to your app", "totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code", "totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized", "unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again", "unauthorizedButton": "Try again"
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
"cancelTitle": "Cancel"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,10 +39,7 @@
"totpSuccessSubtitle": "Redirecting to your app", "totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code", "totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized", "unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again", "unauthorizedButton": "Try again"
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
"cancelTitle": "Cancel"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Se connecter", "loginSubmit": "Se connecter",
"loginFailTitle": "Échec de la connexion", "loginFailTitle": "Échec de la connexion",
"loginFailSubtitle": "Veuillez vérifier votre nom d'utilisateur et votre mot de passe", "loginFailSubtitle": "Veuillez vérifier votre nom d'utilisateur et votre mot de passe",
"loginFailRateLimit": "Vous n'avez pas pu vous connecter trop de fois, veuillez réessayer plus tard",
"loginSuccessTitle": "Connecté", "loginSuccessTitle": "Connecté",
"loginSuccessSubtitle": "Bienvenue!", "loginSuccessSubtitle": "Bienvenue!",
"loginOauthFailTitle": "Erreur interne", "loginOauthFailTitle": "Erreur interne",
@@ -40,10 +39,7 @@
"totpSuccessSubtitle": "Redirection vers votre application", "totpSuccessSubtitle": "Redirection vers votre application",
"totpTitle": "Saisissez votre code TOTP", "totpTitle": "Saisissez votre code TOTP",
"unauthorizedTitle": "Non autorisé", "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>.", "unauthorizedResourceSubtitle": "L'utilisateur avec le nom d'utilisateur {{username}} 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.", "unaothorizedLoginSubtitle": "L'utilisateur avec le nom d'utilisateur {{username}} n'est pas autorisé à se connecter.",
"unauthorizedButton": "Réessayer", "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"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,10 +39,7 @@
"totpSuccessSubtitle": "Redirecting to your app", "totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code", "totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized", "unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again", "unauthorizedButton": "Try again"
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
"cancelTitle": "Cancel"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,10 +39,7 @@
"totpSuccessSubtitle": "Redirecting to your app", "totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code", "totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized", "unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again", "unauthorizedButton": "Try again"
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
"cancelTitle": "Cancel"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,10 +39,7 @@
"totpSuccessSubtitle": "Redirecting to your app", "totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code", "totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized", "unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again", "unauthorizedButton": "Try again"
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
"cancelTitle": "Cancel"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,10 +39,7 @@
"totpSuccessSubtitle": "Redirecting to your app", "totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code", "totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized", "unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again", "unauthorizedButton": "Try again"
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
"cancelTitle": "Cancel"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,10 +39,7 @@
"totpSuccessSubtitle": "Redirecting to your app", "totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code", "totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized", "unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again", "unauthorizedButton": "Try again"
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
"cancelTitle": "Cancel"
} }

View File

@@ -1,49 +1,45 @@
{ {
"loginTitle": "Welkom terug, log in met", "loginTitle": "Welcome back, login with",
"loginDivider": "Of ga door met wachtwoord", "loginDivider": "Or continue with password",
"loginUsername": "Gebruikersnaam", "loginUsername": "Username",
"loginPassword": "Wachtwoord", "loginPassword": "Password",
"loginSubmit": "Log in", "loginSubmit": "Login",
"loginFailTitle": "Mislukt om in te loggen", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Controleer je gebruikersnaam en wachtwoord", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "Inloggen te vaak mislukt, probeer het later opnieuw", "loginSuccessTitle": "Logged in",
"loginSuccessTitle": "Ingelogd", "loginSuccessSubtitle": "Welcome back!",
"loginSuccessSubtitle": "Welkom terug!", "loginOauthFailTitle": "Internal error",
"loginOauthFailTitle": "Interne fout", "loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthFailSubtitle": "Fout bij het ophalen van OAuth URL", "loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessTitle": "Omleiden", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthSuccessSubtitle": "Omleiden naar je OAuth provider", "continueRedirectingTitle": "Redirecting...",
"continueRedirectingTitle": "Omleiden...", "continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectingSubtitle": "Je wordt naar de app doorgestuurd", "continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectTitle": "Ongeldige omleiding", "continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInvalidRedirectSubtitle": "De omleidings-URL is ongeldig", "continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectTitle": "Onveilige doorverwijzing", "continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
"continueInsecureRedirectSubtitle": "Je probeert door te verwijzen van <Code>https</Code> naar <Code>http</Code>, weet je zeker dat je wilt doorgaan?", "continueTitle": "Continue",
"continueTitle": "Ga verder", "continueSubtitle": "Click the button to continue to your app.",
"continueSubtitle": "Klik op de knop om door te gaan naar de app.", "internalErrorTitle": "Internal Server Error",
"internalErrorTitle": "Interne server fout", "internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
"internalErrorSubtitle": "Er is een fout opgetreden op de server en het kan momenteel niet voldoen aan je verzoek.", "internalErrorButton": "Try again",
"internalErrorButton": "Opnieuw proberen", "logoutFailTitle": "Failed to log out",
"logoutFailTitle": "Afmelden mislukt", "logoutFailSubtitle": "Please try again",
"logoutFailSubtitle": "Probeer het opnieuw", "logoutSuccessTitle": "Logged out",
"logoutSuccessTitle": "Afgemeld", "logoutSuccessSubtitle": "You have been logged out",
"logoutSuccessSubtitle": "Je bent afgemeld", "logoutTitle": "Logout",
"logoutTitle": "Afmelden", "logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
"logoutUsernameSubtitle": "Je bent momenteel ingelogd als <Code>{{username}}</Code>, klik op de knop hieronder om uit te loggen.", "logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
"logoutOauthSubtitle": "Je bent momenteel ingelogd als <Code>{{username}}</Code> met behulp van de {{provider}} OAuth provider, klik op de knop hieronder om uit te loggen.", "notFoundTitle": "Page not found",
"notFoundTitle": "Pagina niet gevonden", "notFoundSubtitle": "The page you are looking for does not exist.",
"notFoundSubtitle": "De pagina die je zoekt bestaat niet.", "notFoundButton": "Go home",
"notFoundButton": "Naar startpagina", "totpFailTitle": "Failed to verify code",
"totpFailTitle": "Verifiëren van code mislukt", "totpFailSubtitle": "Please check your code and try again",
"totpFailSubtitle": "Controleer je code en probeer het opnieuw", "totpSuccessTitle": "Verified",
"totpSuccessTitle": "Geverifiëerd", "totpSuccessSubtitle": "Redirecting to your app",
"totpSuccessSubtitle": "Omleiden naar je app", "totpTitle": "Enter your TOTP code",
"totpTitle": "Voer je TOTP-code in", "unauthorizedTitle": "Unauthorized",
"unauthorizedTitle": "Ongeautoriseerd", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unauthorizedResourceSubtitle": "De gebruiker met gebruikersnaam <Code>{{username}}</Code> heeft geen toegang tot de bron <Code>{{resource}}</Code>.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unaothorizedLoginSubtitle": "De gebruiker met gebruikersnaam <Code>{{username}}</Code> is niet gemachtigd om in te loggen.", "unauthorizedButton": "Try again"
"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"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,10 +39,7 @@
"totpSuccessSubtitle": "Redirecting to your app", "totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code", "totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized", "unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again", "unauthorizedButton": "Try again"
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
"cancelTitle": "Cancel"
} }

View File

@@ -3,10 +3,9 @@
"loginDivider": "Lub kontynuuj z hasłem", "loginDivider": "Lub kontynuuj z hasłem",
"loginUsername": "Nazwa użytkownika", "loginUsername": "Nazwa użytkownika",
"loginPassword": "Hasło", "loginPassword": "Hasło",
"loginSubmit": "Zaloguj się", "loginSubmit": "Login",
"loginFailTitle": "Nie udało się zalogować", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Sprawdź swoją nazwę użytkownika i hasło", "loginFailSubtitle": "Sprawdź swoją nazwę użytkownika i hasło",
"loginFailRateLimit": "Nie udało się zalogować zbyt wiele razy, spróbuj ponownie później",
"loginSuccessTitle": "Zalogowano", "loginSuccessTitle": "Zalogowano",
"loginSuccessSubtitle": "Witaj ponownie!", "loginSuccessSubtitle": "Witaj ponownie!",
"loginOauthFailTitle": "Wewnętrzny błąd", "loginOauthFailTitle": "Wewnętrzny błąd",
@@ -22,9 +21,9 @@
"continueTitle": "Kontynuuj", "continueTitle": "Kontynuuj",
"continueSubtitle": "Kliknij przycisk, aby przejść do aplikacji.", "continueSubtitle": "Kliknij przycisk, aby przejść do aplikacji.",
"internalErrorTitle": "Wewnętrzny błąd serwera", "internalErrorTitle": "Wewnętrzny błąd serwera",
"internalErrorSubtitle": "Wystąpił błąd na serwerze i obecnie nie można obsłużyć tego żądania.", "internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
"internalErrorButton": "Spróbuj ponownie", "internalErrorButton": "Spróbuj ponownie",
"logoutFailTitle": "Nie udało się wylogować", "logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Spróbuj ponownie", "logoutFailSubtitle": "Spróbuj ponownie",
"logoutSuccessTitle": "Wylogowano", "logoutSuccessTitle": "Wylogowano",
"logoutSuccessSubtitle": "Zostałeś wylogowany", "logoutSuccessSubtitle": "Zostałeś wylogowany",
@@ -40,10 +39,7 @@
"totpSuccessSubtitle": "Przekierowywanie do aplikacji", "totpSuccessSubtitle": "Przekierowywanie do aplikacji",
"totpTitle": "Wprowadź kod TOTP", "totpTitle": "Wprowadź kod TOTP",
"unauthorizedTitle": "Nieautoryzowany", "unauthorizedTitle": "Nieautoryzowany",
"unauthorizedResourceSubtitle": "Użytkownik o nazwie <Code>{{username}}</Code> nie jest upoważniony do uzyskania dostępu do zasobu <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "Użytkownik o nazwie {{username}} 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.", "unaothorizedLoginSubtitle": "Użytkownik o nazwie {{username}} nie jest upoważniony do logowania.",
"unauthorizedButton": "Spróbuj ponownie", "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"
} }

View File

@@ -1,49 +1,45 @@
{ {
"loginTitle": "Bem-vindo de volta, faça o login com", "loginTitle": "Welcome back, login with",
"loginDivider": "Ou continuar com uma senha", "loginDivider": "Or continue with password",
"loginUsername": "Nome de usuário", "loginUsername": "Username",
"loginPassword": "Senha", "loginPassword": "Password",
"loginSubmit": "Entrar", "loginSubmit": "Login",
"loginFailTitle": "Falha ao iniciar sessão", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Por favor, verifique seu usuário e senha", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "Você falhou em iniciar sessão muitas vezes, por favor tente novamente mais tarde", "loginSuccessTitle": "Logged in",
"loginSuccessTitle": "Sessão Iniciada", "loginSuccessSubtitle": "Welcome back!",
"loginSuccessSubtitle": "Bem-vindo de volta!", "loginOauthFailTitle": "Internal error",
"loginOauthFailTitle": "Erro interno", "loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthFailSubtitle": "Falha ao obter URL de OAuth", "loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessTitle": "Redirecionando", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthSuccessSubtitle": "Redirecionando para seu provedor OAuth", "continueRedirectingTitle": "Redirecting...",
"continueRedirectingTitle": "Redirecionando...", "continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectingSubtitle": "Você deve ser redirecionado para o aplicativo em breve", "continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectTitle": "Redirecionamento inválido", "continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInvalidRedirectSubtitle": "O endereço de redirecionamento é inválido", "continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectTitle": "Redirecionamento inseguro", "continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
"continueInsecureRedirectSubtitle": "Você está tentando redirecionar de <Code>https</Code> para <Code>http</Code>, você tem certeza que deseja continuar?", "continueTitle": "Continue",
"continueTitle": "Continuar", "continueSubtitle": "Click the button to continue to your app.",
"continueSubtitle": "Clique no botão para continuar para o seu aplicativo.", "internalErrorTitle": "Internal Server Error",
"internalErrorTitle": "Erro interno do servidor", "internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
"internalErrorSubtitle": "Ocorreu um erro no servidor e atualmente não pode servir sua solicitação.", "internalErrorButton": "Try again",
"internalErrorButton": "Tentar novamente", "logoutFailTitle": "Failed to log out",
"logoutFailTitle": "Falha ao encerrar sessão", "logoutFailSubtitle": "Please try again",
"logoutFailSubtitle": "Por favor, tente novamente", "logoutSuccessTitle": "Logged out",
"logoutSuccessTitle": "Sessão encerrada", "logoutSuccessSubtitle": "You have been logged out",
"logoutSuccessSubtitle": "Você foi desconectado", "logoutTitle": "Logout",
"logoutTitle": "Sair", "logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
"logoutUsernameSubtitle": "Você está atualmente logado como <Code>{{username}}</Code>, clique no botão abaixo para sair.", "logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
"logoutOauthSubtitle": "Você está atualmente logado como <Code>{{username}}</Code> usando o provedor {{provider}} OAuth, clique no botão abaixo para sair.", "notFoundTitle": "Page not found",
"notFoundTitle": "Página não encontrada", "notFoundSubtitle": "The page you are looking for does not exist.",
"notFoundSubtitle": "A página que você está procurando não existe.", "notFoundButton": "Go home",
"notFoundButton": "Voltar para a tela inicial", "totpFailTitle": "Failed to verify code",
"totpFailTitle": "Falha ao verificar código", "totpFailSubtitle": "Please check your code and try again",
"totpFailSubtitle": "Por favor, verifique seu código e tente novamente", "totpSuccessTitle": "Verified",
"totpSuccessTitle": "Verificado", "totpSuccessSubtitle": "Redirecting to your app",
"totpSuccessSubtitle": "Redirecionando para o seu aplicativo", "totpTitle": "Enter your TOTP code",
"totpTitle": "Insira o seu código TOTP", "unauthorizedTitle": "Unauthorized",
"unauthorizedTitle": "Não autorizado", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unauthorizedResourceSubtitle": "O usuário com nome de usuário <Code>{{username}}</Code> não está autorizado a acessar o recurso <Code>{{resource}}</Code>.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unaothorizedLoginSubtitle": "O usuário com o nome <Code>{{username}}</Code> não está autorizado a acessar.", "unauthorizedButton": "Try again"
"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"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,10 +39,7 @@
"totpSuccessSubtitle": "Redirecting to your app", "totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code", "totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized", "unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again", "unauthorizedButton": "Try again"
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
"cancelTitle": "Cancel"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,10 +39,7 @@
"totpSuccessSubtitle": "Redirecting to your app", "totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code", "totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized", "unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again", "unauthorizedButton": "Try again"
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
"cancelTitle": "Cancel"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,10 +39,7 @@
"totpSuccessSubtitle": "Redirecting to your app", "totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code", "totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized", "unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again", "unauthorizedButton": "Try again"
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
"cancelTitle": "Cancel"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,10 +39,7 @@
"totpSuccessSubtitle": "Redirecting to your app", "totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code", "totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized", "unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again", "unauthorizedButton": "Try again"
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
"cancelTitle": "Cancel"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,10 +39,7 @@
"totpSuccessSubtitle": "Redirecting to your app", "totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code", "totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized", "unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again", "unauthorizedButton": "Try again"
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
"cancelTitle": "Cancel"
} }

View File

@@ -1,49 +1,45 @@
{ {
"loginTitle": "Welcome back, login with", "loginTitle": "Welcome back, login with",
"loginDivider": "Ya da şifre ile devam edin", "loginDivider": "Or continue with password",
"loginUsername": "Kullanıcı Adı", "loginUsername": "Username",
"loginPassword": "Şifre", "loginPassword": "Password",
"loginSubmit": "Giriş Yap", "loginSubmit": "Login",
"loginFailTitle": "Giriş yapılamadı", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later", "loginSuccessTitle": "Logged in",
"loginSuccessTitle": "Giriş yapıldı", "loginSuccessSubtitle": "Welcome back!",
"loginSuccessSubtitle": "Tekrar hoş geldiniz!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
"loginOauthFailSubtitle": "Failed to get OAuth URL", "loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Yönlendiriliyor", "loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"continueRedirectingTitle": "Yönlendiriliyor...", "continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon", "continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueInvalidRedirectTitle": "Invalid redirect", "continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectSubtitle": "The redirect URL is invalid", "continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInsecureRedirectTitle": "Insecure redirect", "continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?", "continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
"continueTitle": "Devam et", "continueTitle": "Continue",
"continueSubtitle": "Click the button to continue to your app.", "continueSubtitle": "Click the button to continue to your app.",
"internalErrorTitle": "İç Sunucu Hatası", "internalErrorTitle": "Internal Server Error",
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.", "internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
"internalErrorButton": "Tekrar deneyin", "internalErrorButton": "Try again",
"logoutFailTitle": "Failed to log out", "logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Lütfen tekrar deneyin", "logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Çıkış yapıldı", "logoutSuccessTitle": "Logged out",
"logoutSuccessSubtitle": "You have been logged out", "logoutSuccessSubtitle": "You have been logged out",
"logoutTitle": "Logout", "logoutTitle": "Logout",
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.", "logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.", "logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
"notFoundTitle": "Sayfa bulunamadı", "notFoundTitle": "Page not found",
"notFoundSubtitle": "Aradığınız sayfa mevcut değil.", "notFoundSubtitle": "The page you are looking for does not exist.",
"notFoundButton": "Ana sayfaya git", "notFoundButton": "Go home",
"totpFailTitle": "Kod doğrulanamadı", "totpFailTitle": "Failed to verify code",
"totpFailSubtitle": "Please check your code and try again", "totpFailSubtitle": "Please check your code and try again",
"totpSuccessTitle": "Doğrulandı", "totpSuccessTitle": "Verified",
"totpSuccessSubtitle": "Redirecting to your app", "totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code", "totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized", "unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again", "unauthorizedButton": "Try again"
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
"cancelTitle": "İptal"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,10 +39,7 @@
"totpSuccessSubtitle": "Redirecting to your app", "totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code", "totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized", "unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again", "unauthorizedButton": "Try again"
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
"cancelTitle": "Cancel"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,10 +39,7 @@
"totpSuccessSubtitle": "Redirecting to your app", "totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code", "totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized", "unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again", "unauthorizedButton": "Try again"
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
"cancelTitle": "Cancel"
} }

View File

@@ -1,49 +1,45 @@
{ {
"loginTitle": "欢迎回来,请登录", "loginTitle": "Welcome back, login with",
"loginDivider": "或者继续使用密码", "loginDivider": "Or continue with password",
"loginUsername": "用户名", "loginUsername": "Username",
"loginPassword": "密码", "loginPassword": "Password",
"loginSubmit": "登录", "loginSubmit": "Login",
"loginFailTitle": "登录失败", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "请检查您的用户名和密码", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "您登录次数过多,请稍后再试", "loginSuccessTitle": "Logged in",
"loginSuccessTitle": "已登录", "loginSuccessSubtitle": "Welcome back!",
"loginSuccessSubtitle": "欢迎回来!", "loginOauthFailTitle": "Internal error",
"loginOauthFailTitle": "内部错误", "loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthFailSubtitle": "获取 OAuth URL 失败", "loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessTitle": "重定向中", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthSuccessSubtitle": "重定向到您的 OAuth 提供商", "continueRedirectingTitle": "Redirecting...",
"continueRedirectingTitle": "正在重定向……", "continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectingSubtitle": "您应该很快被重定向到应用", "continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectTitle": "无效的重定向", "continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInvalidRedirectSubtitle": "重定向URL无效", "continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectTitle": "不安全的重定向", "continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
"continueInsecureRedirectSubtitle": "您正在尝试将 <Code>https</Code> 重定向到 <Code>http</Code>,您确定要继续吗?", "continueTitle": "Continue",
"continueTitle": "继续", "continueSubtitle": "Click the button to continue to your app.",
"continueSubtitle": "点击按钮以继续您的应用。", "internalErrorTitle": "Internal Server Error",
"internalErrorTitle": "服务器内部错误", "internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
"internalErrorSubtitle": "服务器上发生错误,当前无法满足您的请求。", "internalErrorButton": "Try again",
"internalErrorButton": "重试", "logoutFailTitle": "Failed to log out",
"logoutFailTitle": "注销失败", "logoutFailSubtitle": "Please try again",
"logoutFailSubtitle": "请重试", "logoutSuccessTitle": "Logged out",
"logoutSuccessTitle": "已登出", "logoutSuccessSubtitle": "You have been logged out",
"logoutSuccessSubtitle": "您已登出", "logoutTitle": "Logout",
"logoutTitle": "登出", "logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
"logoutUsernameSubtitle": "您当前以 <Code>{{username}}</Code> 的身份登录,点击下方按钮退出登录。", "logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
"logoutOauthSubtitle": "您当前以 <Code>{{username}}</Code> 的身份登录,使用的是 {{provider}} OAuth 提供商,点击下方按钮退出登录。", "notFoundTitle": "Page not found",
"notFoundTitle": "无法找到页面", "notFoundSubtitle": "The page you are looking for does not exist.",
"notFoundSubtitle": "您正在查找的页面不存在。", "notFoundButton": "Go home",
"notFoundButton": "回到主页", "totpFailTitle": "Failed to verify code",
"totpFailTitle": "无法验证代码", "totpFailSubtitle": "Please check your code and try again",
"totpFailSubtitle": "请检查您的代码并重试", "totpSuccessTitle": "Verified",
"totpSuccessTitle": "已验证", "totpSuccessSubtitle": "Redirecting to your app",
"totpSuccessSubtitle": "重定向到您的应用", "totpTitle": "Enter your TOTP code",
"totpTitle": "输入您的 TOTP 代码", "unauthorizedTitle": "Unauthorized",
"unauthorizedTitle": "未授权", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unauthorizedResourceSubtitle": "用户 <Code>{{username}}</Code> 无权访问资源 <Code>{{resource}}</Code>。", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unaothorizedLoginSubtitle": "用户名 <Code>{{username}}</Code> 无登录权限。", "unauthorizedButton": "Try again"
"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"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,10 +39,7 @@
"totpSuccessSubtitle": "Redirecting to your app", "totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code", "totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized", "unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again", "unauthorizedButton": "Try again"
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
"cancelTitle": "Cancel"
} }

View File

@@ -18,14 +18,18 @@ import { InternalServerError } from "./pages/internal-server-error.tsx";
import { TotpPage } from "./pages/totp-page.tsx"; import { TotpPage } from "./pages/totp-page.tsx";
import { AppContextProvider } from "./context/app-context.tsx"; import { AppContextProvider } from "./context/app-context.tsx";
import "./lib/i18n/i18n.ts"; import "./lib/i18n/i18n.ts";
import { ForgotPasswordPage } from "./pages/forgot-password-page.tsx";
import "./index.css";
const queryClient = new QueryClient(); const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true,
},
},
});
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>
<MantineProvider defaultColorScheme="auto"> <MantineProvider forceColorScheme="dark">
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<Notifications /> <Notifications />
<AppContextProvider> <AppContextProvider>
@@ -39,10 +43,6 @@ createRoot(document.getElementById("root")!).render(
<Route path="/continue" element={<ContinuePage />} /> <Route path="/continue" element={<ContinuePage />} />
<Route path="/unauthorized" element={<UnauthorizedPage />} /> <Route path="/unauthorized" element={<UnauthorizedPage />} />
<Route path="/error" element={<InternalServerError />} /> <Route path="/error" element={<InternalServerError />} />
<Route
path="/forgot-password"
element={<ForgotPasswordPage />}
/>
<Route path="*" element={<NotFoundPage />} /> <Route path="*" element={<NotFoundPage />} />
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>

View File

@@ -4,7 +4,7 @@ import { Navigate } from "react-router";
import { useUserContext } from "../context/user-context"; import { useUserContext } from "../context/user-context";
import { Layout } from "../components/layouts/layout"; import { Layout } from "../components/layouts/layout";
import { ReactNode } from "react"; import { ReactNode } from "react";
import { escapeRegex, isQueryValid } from "../utils/utils"; import { isQueryValid } from "../utils/utils";
import { useAppContext } from "../context/app-context"; import { useAppContext } from "../context/app-context";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
@@ -14,7 +14,7 @@ export const ContinuePage = () => {
const redirectUri = params.get("redirect_uri") ?? ""; const redirectUri = params.get("redirect_uri") ?? "";
const { isLoggedIn } = useUserContext(); const { isLoggedIn } = useUserContext();
const { disableContinue, domain } = useAppContext(); const { disableContinue } = useAppContext();
const { t } = useTranslation(); const { t } = useTranslation();
if (!isLoggedIn) { if (!isLoggedIn) {
@@ -51,30 +51,6 @@ export const ContinuePage = () => {
); );
} }
const regex = new RegExp(`^.*${escapeRegex(domain)}$`)
if (!regex.test(uri.hostname)) {
return (
<ContinuePageLayout>
<Text size="xl" fw={700}>
{t("untrustedRedirectTitle")}
</Text>
<Trans
i18nKey="untrustedRedirectSubtitle"
t={t}
components={{ Code: <Code /> }}
values={{ domain: domain }}
/>
<Button fullWidth mt="xl" color="red" onClick={redirect}>
{t('continueTitle')}
</Button>
<Button fullWidth mt="sm" color="gray" onClick={() => window.location.href = "/"}>
{t('cancelTitle')}
</Button>
</ContinuePageLayout>
)
}
if (disableContinue) { if (disableContinue) {
window.location.href = redirectUri; window.location.href = redirectUri;
return ( return (
@@ -103,9 +79,6 @@ export const ContinuePage = () => {
<Button fullWidth mt="xl" color="yellow" onClick={redirect}> <Button fullWidth mt="xl" color="yellow" onClick={redirect}>
{t("continueTitle")} {t("continueTitle")}
</Button> </Button>
<Button fullWidth mt="sm" color="gray" onClick={() => window.location.href = "/"}>
{t('cancelTitle')}
</Button>
</ContinuePageLayout> </ContinuePageLayout>
); );
} }

View File

@@ -1,25 +0,0 @@
import { Paper, Text, TypographyStylesProvider } from "@mantine/core";
import { Layout } from "../components/layouts/layout";
import { useTranslation } from "react-i18next";
import { useAppContext } from "../context/app-context";
import Markdown from 'react-markdown'
export const ForgotPasswordPage = () => {
const { t } = useTranslation();
const { forgotPasswordMessage } = useAppContext();
return (
<Layout>
<Paper shadow="md" p={30} mt={30} radius="md" withBorder>
<Text size="xl" fw={700}>
{t("forgotPasswordTitle")}
</Text>
<TypographyStylesProvider>
<Markdown>
{forgotPasswordMessage}
</Markdown>
</TypographyStylesProvider>
</Paper>
</Layout>
);
};

View File

@@ -1,7 +1,7 @@
import { Paper, Title, Text, Divider } from "@mantine/core"; import { Paper, Title, Text, Divider } from "@mantine/core";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import axios, { type AxiosError } from "axios"; import axios from "axios";
import { useUserContext } from "../context/user-context"; import { useUserContext } from "../context/user-context";
import { Navigate } from "react-router"; import { Navigate } from "react-router";
import { Layout } from "../components/layouts/layout"; import { Layout } from "../components/layouts/layout";
@@ -33,17 +33,7 @@ export const LoginPage = () => {
mutationFn: (login: LoginFormValues) => { mutationFn: (login: LoginFormValues) => {
return axios.post("/api/login", login); return axios.post("/api/login", login);
}, },
onError: (data: AxiosError) => { onError: () => {
if (data.response) {
if (data.response.status === 429) {
notifications.show({
title: t("loginFailTitle"),
message: t("loginFailRateLimit"),
color: "red",
});
return;
}
}
notifications.show({ notifications.show({
title: t("loginFailTitle"), title: t("loginFailTitle"),
message: t("loginFailSubtitle"), message: t("loginFailSubtitle"),
@@ -113,7 +103,7 @@ export const LoginPage = () => {
</Text> </Text>
<OAuthButtons <OAuthButtons
oauthProviders={oauthProviders} oauthProviders={oauthProviders}
isPending={loginOAuthMutation.isPending} isLoading={loginOAuthMutation.isLoading}
mutate={loginOAuthMutation.mutate} mutate={loginOAuthMutation.mutate}
genericName={genericName} genericName={genericName}
/> />
@@ -128,7 +118,7 @@ export const LoginPage = () => {
)} )}
{configuredProviders.includes("username") && ( {configuredProviders.includes("username") && (
<LoginForm <LoginForm
isPending={loginMutation.isPending} isLoading={loginMutation.isLoading}
onSubmit={handleSubmit} onSubmit={handleSubmit}
/> />
)} )}

View File

@@ -10,7 +10,7 @@ import { useAppContext } from "../context/app-context";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
export const LogoutPage = () => { export const LogoutPage = () => {
const { isLoggedIn, oauth, provider, email, username } = useUserContext(); const { isLoggedIn, username, oauth, provider } = useUserContext();
const { genericName } = useAppContext(); const { genericName } = useAppContext();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -56,7 +56,7 @@ export const LogoutPage = () => {
values={{ values={{
provider: provider:
provider === "generic" ? genericName : capitalize(provider), provider === "generic" ? genericName : capitalize(provider),
username: email, username: username,
}} }}
/> />
) : ( ) : (
@@ -74,7 +74,7 @@ export const LogoutPage = () => {
fullWidth fullWidth
mt="xl" mt="xl"
onClick={() => logoutMutation.mutate()} onClick={() => logoutMutation.mutate()}
loading={logoutMutation.isPending} loading={logoutMutation.isLoading}
> >
{t("logoutTitle")} {t("logoutTitle")}
</Button> </Button>

View File

@@ -57,7 +57,7 @@ export const TotpPage = () => {
{t("totpTitle")} {t("totpTitle")}
</Text> </Text>
<TotpForm <TotpForm
isPending={totpMutation.isPending} isLoading={totpMutation.isLoading}
onSubmit={(values) => totpMutation.mutate(values)} onSubmit={(values) => totpMutation.mutate(values)}
/> />
</Paper> </Paper>

View File

@@ -3,13 +3,11 @@ import { Layout } from "../components/layouts/layout";
import { Navigate } from "react-router"; import { Navigate } from "react-router";
import { isQueryValid } from "../utils/utils"; import { isQueryValid } from "../utils/utils";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import React from "react";
export const UnauthorizedPage = () => { export const UnauthorizedPage = () => {
const queryString = window.location.search; const queryString = window.location.search;
const params = new URLSearchParams(queryString); const params = new URLSearchParams(queryString);
const username = params.get("username") ?? ""; const username = params.get("username") ?? "";
const groupErr = params.get("groupErr") ?? "";
const resource = params.get("resource") ?? ""; const resource = params.get("resource") ?? "";
const { t } = useTranslation(); const { t } = useTranslation();
@@ -18,54 +16,32 @@ export const UnauthorizedPage = () => {
return <Navigate to="/" />; return <Navigate to="/" />;
} }
if (isQueryValid(resource) && !isQueryValid(groupErr)) {
return (
<UnauthorizedLayout>
<Trans
i18nKey="unauthorizedResourceSubtitle"
t={t}
components={{ Code: <Code /> }}
values={{ resource, username }}
/>
</UnauthorizedLayout>
);
}
if (isQueryValid(groupErr) && isQueryValid(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 ( return (
<Layout> <Layout>
<Paper shadow="md" p={30} mt={30} radius="md" withBorder> <Paper shadow="md" p={30} mt={30} radius="md" withBorder>
<Text size="xl" fw={700}> <Text size="xl" fw={700}>
{t("Unauthorized")} {t("Unauthorized")}
</Text> </Text>
<Text>{children}</Text> <Text>
{isQueryValid(resource) ? (
<Text>
<Trans
i18nKey="unauthorizedResourceSubtitle"
t={t}
components={{ Code: <Code /> }}
values={{ resource, username }}
/>
</Text>
) : (
<Text>
<Trans
i18nKey="unauthorizedLoginSubtitle"
t={t}
values={{ username }}
/>
</Text>
)}
</Text>
<Button <Button
fullWidth fullWidth
mt="xl" mt="xl"

View File

@@ -5,8 +5,6 @@ export const appContextSchema = z.object({
disableContinue: z.boolean(), disableContinue: z.boolean(),
title: z.string(), title: z.string(),
genericName: z.string(), genericName: z.string(),
domain: z.string(),
forgotPasswordMessage: z.string(),
}); });
export type AppContextSchemaType = z.infer<typeof appContextSchema>; export type AppContextSchemaType = z.infer<typeof appContextSchema>;

View File

@@ -3,8 +3,6 @@ import { z } from "zod";
export const userContextSchema = z.object({ export const userContextSchema = z.object({
isLoggedIn: z.boolean(), isLoggedIn: z.boolean(),
username: z.string(), username: z.string(),
name: z.string(),
email: z.string(),
oauth: z.boolean(), oauth: z.boolean(),
provider: z.string(), provider: z.string(),
totpPending: z.boolean(), totpPending: z.boolean(),

View File

@@ -1,3 +1,2 @@
export const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); export const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
export const isQueryValid = (value: string) => value.trim() !== "" && value !== "null"; export const isQueryValid = (value: string) => value.trim() !== "" && value !== "null";
export const escapeRegex = (value: string) => value.replace(/[-\/\\^$.*+?()[\]{}|]/g, "\\$&");

68
go.mod
View File

@@ -4,28 +4,23 @@ go 1.23.2
require ( require (
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.10.0
github.com/go-playground/validator/v10 v10.26.0 github.com/go-playground/validator/v10 v10.24.0
github.com/google/go-querystring v1.1.0 github.com/google/go-querystring v1.1.0
github.com/mdp/qrterminal/v3 v3.2.1 github.com/mdp/qrterminal/v3 v3.2.0
github.com/rs/zerolog v1.34.0 github.com/rs/zerolog v1.33.0
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.20.1 github.com/spf13/viper v1.19.0
golang.org/x/crypto v0.37.0 golang.org/x/crypto v0.32.0
) )
require ( require (
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/containerd/log v0.1.0 // indirect github.com/containerd/log v0.1.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect github.com/moby/term v0.5.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect github.com/morikuni/aec v1.0.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect
go.opentelemetry.io/otel/sdk v1.34.0 // indirect go.opentelemetry.io/otel/sdk v1.34.0 // indirect
golang.org/x/term v0.31.0 // indirect golang.org/x/term v0.28.0 // indirect
gotest.tools/v3 v3.5.2 // indirect gotest.tools/v3 v3.5.2 // indirect
rsc.io/qr v0.2.0 // indirect rsc.io/qr v0.2.0 // indirect
) )
@@ -37,23 +32,23 @@ require (
github.com/boombuler/barcode v1.0.2 // indirect github.com/boombuler/barcode v1.0.2 // indirect
github.com/bytedance/sonic v1.12.7 // indirect github.com/bytedance/sonic v1.12.7 // indirect
github.com/bytedance/sonic/loader v0.2.3 // indirect github.com/bytedance/sonic/loader v0.2.3 // indirect
github.com/catppuccin/go v0.3.0 // indirect github.com/catppuccin/go v0.2.0 // indirect
github.com/charmbracelet/bubbles v0.21.0 // indirect github.com/charmbracelet/bubbles v0.20.0 // indirect
github.com/charmbracelet/bubbletea v1.3.4 // indirect github.com/charmbracelet/bubbletea v1.1.0 // indirect
github.com/charmbracelet/huh v0.7.0 github.com/charmbracelet/huh v0.6.0
github.com/charmbracelet/lipgloss v1.1.0 // indirect github.com/charmbracelet/lipgloss v0.13.0 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/charmbracelet/x/ansi v0.2.3 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect github.com/charmbracelet/x/term v0.2.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/base64x v0.1.4 // indirect
github.com/distribution/reference v0.6.0 // indirect github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v28.1.1+incompatible github.com/docker/docker v27.5.1+incompatible
github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect github.com/gin-contrib/sse v1.0.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/logr v1.4.2 // indirect
@@ -63,50 +58,55 @@ require (
github.com/goccy/go-json v0.10.4 // indirect github.com/goccy/go-json v0.10.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.4.0 github.com/gorilla/sessions v1.2.2
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/magiconair/properties v1.8.10 github.com/magiconair/properties v1.8.7
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pquerna/otp v1.4.0 github.com/pquerna/otp v1.4.0
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.7.1 // indirect github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.34.0 // indirect go.opentelemetry.io/otel v1.34.0 // indirect
go.opentelemetry.io/otel/metric v1.34.0 // indirect go.opentelemetry.io/otel/metric v1.34.0 // indirect
go.opentelemetry.io/otel/trace v1.34.0 // indirect go.opentelemetry.io/otel/trace v1.34.0 // indirect
go.uber.org/atomic v1.9.0 // indirect go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.13.0 // indirect golang.org/x/arch v0.13.0 // indirect
golang.org/x/net v0.38.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/oauth2 v0.29.0 golang.org/x/net v0.34.0 // indirect
golang.org/x/sync v0.13.0 // indirect golang.org/x/oauth2 v0.25.0
golang.org/x/sys v0.32.0 // indirect golang.org/x/sync v0.10.0 // indirect
golang.org/x/text v0.24.0 // indirect golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.36.3 // indirect google.golang.org/protobuf v1.36.3 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

170
go.sum
View File

@@ -8,8 +8,6 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4= github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
@@ -18,54 +16,39 @@ github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOE
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c=
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo= github.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU=
github.com/charmbracelet/huh v0.7.0 h1:W8S1uyGETgj9Tuda3/JdVkc3x7DBLZYPZc4c+/rnRdc= github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
github.com/charmbracelet/huh v0.7.0/go.mod h1:UGC3DZHlgOKHvHC07a5vHag41zzhpPFj34U92sOmyuk= github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I= github.com/docker/docker v27.5.1+incompatible h1:4PYU5dnBYqRQi0294d1FBECqT9ECWeQAIfE8q4YnPY8=
github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v27.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -78,8 +61,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
@@ -97,10 +80,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -118,10 +99,12 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -141,8 +124,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
@@ -154,16 +137,14 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4= github.com/mdp/qrterminal/v3 v3.2.0 h1:qteQMXO3oyTK4IHwj2mWsKYYRBOp1Pj2WRYFYYNTCdk=
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU= github.com/mdp/qrterminal/v3 v3.2.0/go.mod h1:XGGuua4Lefrl7TLEsSONiD+UEjQXJZ4mPzF+gWYIJkk=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -177,8 +158,8 @@ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
@@ -188,8 +169,9 @@ github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xl
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -197,27 +179,29 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -238,14 +222,12 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
@@ -269,25 +251,25 @@ golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -297,16 +279,16 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -315,7 +297,7 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA=
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
@@ -327,6 +309,8 @@ google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojt
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -17,15 +17,15 @@ import (
func NewAPI(config types.APIConfig, handlers *handlers.Handlers) *API { func NewAPI(config types.APIConfig, handlers *handlers.Handlers) *API {
return &API{ return &API{
Config: config,
Handlers: handlers, Handlers: handlers,
Config: config,
} }
} }
type API struct { type API struct {
Config types.APIConfig
Router *gin.Engine Router *gin.Engine
Handlers *handlers.Handlers Handlers *handlers.Handlers
Config types.APIConfig
} }
func (api *API) Init() { func (api *API) Init() {

View File

@@ -19,6 +19,12 @@ import (
"github.com/magiconair/properties/assert" "github.com/magiconair/properties/assert"
) )
// User
var User = types.User{
Username: "user",
Password: "$2a$10$AvGHLTYv3xiRJ0xV9xs3XeVIlkGTygI9nqIamFYB5Xu.5.0UWF7B6", // pass
}
// Simple API config for tests // Simple API config for tests
var apiConfig = types.APIConfig{ var apiConfig = types.APIConfig{
Port: 8080, Port: 8080,
@@ -27,38 +33,29 @@ var apiConfig = types.APIConfig{
// Simple handlers config for tests // Simple handlers config for tests
var handlersConfig = types.HandlersConfig{ var handlersConfig = types.HandlersConfig{
AppURL: "http://localhost:8080", AppURL: "http://localhost:8080",
DisableContinue: false, Domain: ".localhost",
Title: "Tinyauth", CookieSecure: false,
GenericName: "Generic", DisableContinue: false,
ForgotPasswordMessage: "Some message", Title: "Tinyauth",
GenericName: "Generic",
} }
// Simple auth config for tests // Simple auth config for tests
var authConfig = types.AuthConfig{ var authConfig = types.AuthConfig{
Users: types.Users{}, Domain: "localhost",
OauthWhitelist: "", Secret: "super-secret-api-thing-for-tests", // It is 32 chars long
Secret: "super-secret-api-thing-for-tests", // It is 32 chars long CookieSecure: false,
CookieSecure: false, SessionExpiry: 3600,
SessionExpiry: 3600, Users: types.Users{
LoginTimeout: 0, User,
LoginMaxRetries: 0, },
} OAuthWhitelist: []string{},
// Simple hooks config for tests
var hooksConfig = types.HooksConfig{
Domain: "localhost",
} }
// Cookie // Cookie
var cookie string var cookie string
// User
var user = types.User{
Username: "user",
Password: "$2a$10$AvGHLTYv3xiRJ0xV9xs3XeVIlkGTygI9nqIamFYB5Xu.5.0UWF7B6", // pass
}
// We need all this to be able to test the API // We need all this to be able to test the API
func getAPI(t *testing.T) *api.API { func getAPI(t *testing.T) *api.API {
// Create docker service // Create docker service
@@ -73,12 +70,6 @@ func getAPI(t *testing.T) *api.API {
} }
// Create auth service // Create auth service
authConfig.Users = types.Users{
{
Username: user.Username,
Password: user.Password,
},
}
auth := auth.NewAuth(authConfig, docker) auth := auth.NewAuth(authConfig, docker)
// Create providers service // Create providers service
@@ -88,7 +79,7 @@ func getAPI(t *testing.T) *api.API {
providers.Init() providers.Init()
// Create hooks service // Create hooks service
hooks := hooks.NewHooks(hooksConfig, auth, providers) hooks := hooks.NewHooks(auth, providers)
// Create handlers service // Create handlers service
handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker) handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker)
@@ -199,13 +190,12 @@ func TestAppContext(t *testing.T) {
// Create tests values // Create tests values
expected := types.AppContext{ expected := types.AppContext{
Status: 200, Status: 200,
Message: "OK", Message: "OK",
ConfiguredProviders: []string{"username"}, ConfiguredProviders: []string{"username"},
DisableContinue: false, DisableContinue: false,
Title: "Tinyauth", Title: "Tinyauth",
GenericName: "Generic", GenericName: "Generic",
ForgotPasswordMessage: "Some message",
} }
// We should get the username back // We should get the username back

View File

@@ -1 +1 @@
v3.3.0 v3.2.0

View File

@@ -3,12 +3,11 @@ package auth
import ( import (
"fmt" "fmt"
"regexp" "regexp"
"slices"
"strings" "strings"
"sync"
"time" "time"
"tinyauth/internal/docker" "tinyauth/internal/docker"
"tinyauth/internal/types" "tinyauth/internal/types"
"tinyauth/internal/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
@@ -18,40 +17,14 @@ import (
func NewAuth(config types.AuthConfig, docker *docker.Docker) *Auth { func NewAuth(config types.AuthConfig, docker *docker.Docker) *Auth {
return &Auth{ return &Auth{
Config: config, Docker: docker,
Docker: docker, Config: config,
LoginAttempts: make(map[string]*types.LoginAttempt),
} }
} }
type Auth struct { type Auth struct {
Config types.AuthConfig Docker *docker.Docker
Docker *docker.Docker Config types.AuthConfig
LoginAttempts map[string]*types.LoginAttempt
LoginMutex sync.RWMutex
}
func (auth *Auth) GetSession(c *gin.Context) (*sessions.Session, error) {
// Create cookie store
store := sessions.NewCookieStore([]byte(auth.Config.Secret))
// Configure cookie store
store.Options = &sessions.Options{
Path: "/",
MaxAge: auth.Config.SessionExpiry,
Secure: auth.Config.CookieSecure,
HttpOnly: true,
Domain: fmt.Sprintf(".%s", auth.Config.Domain),
}
// Get session
session, err := store.Get(c.Request, "tinyauth")
if err != nil {
log.Error().Err(err).Msg("Failed to get session")
return nil, err
}
return session, nil
} }
func (auth *Auth) GetUser(username string) *types.User { func (auth *Auth) GetUser(username string) *types.User {
@@ -69,81 +42,50 @@ func (auth *Auth) CheckPassword(user types.User, password string) bool {
return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil
} }
// IsAccountLocked checks if a username or IP is locked due to too many failed login attempts
func (auth *Auth) IsAccountLocked(identifier string) (bool, int) {
auth.LoginMutex.RLock()
defer auth.LoginMutex.RUnlock()
// Return false if rate limiting is not configured
if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 {
return false, 0
}
// Check if the identifier exists in the map
attempt, exists := auth.LoginAttempts[identifier]
if !exists {
return false, 0
}
// If account is locked, check if lock time has expired
if attempt.LockedUntil.After(time.Now()) {
// Calculate remaining lockout time in seconds
remaining := int(time.Until(attempt.LockedUntil).Seconds())
return true, remaining
}
// Lock has expired
return false, 0
}
// RecordLoginAttempt records a login attempt for rate limiting
func (auth *Auth) RecordLoginAttempt(identifier string, success bool) {
// Skip if rate limiting is not configured
if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 {
return
}
auth.LoginMutex.Lock()
defer auth.LoginMutex.Unlock()
// Get current attempt record or create a new one
attempt, exists := auth.LoginAttempts[identifier]
if !exists {
attempt = &types.LoginAttempt{}
auth.LoginAttempts[identifier] = attempt
}
// Update last attempt time
attempt.LastAttempt = time.Now()
// If successful login, reset failed attempts
if success {
attempt.FailedAttempts = 0
attempt.LockedUntil = time.Time{} // Reset lock time
return
}
// Increment failed attempts
attempt.FailedAttempts++
// If max retries reached, lock the account
if attempt.FailedAttempts >= auth.Config.LoginMaxRetries {
attempt.LockedUntil = time.Now().Add(time.Duration(auth.Config.LoginTimeout) * time.Second)
log.Warn().Str("identifier", identifier).Int("timeout", auth.Config.LoginTimeout).Msg("Account locked due to too many failed login attempts")
}
}
func (auth *Auth) EmailWhitelisted(emailSrc string) bool { func (auth *Auth) EmailWhitelisted(emailSrc string) bool {
return utils.CheckWhitelist(auth.Config.OauthWhitelist, emailSrc) // If the whitelist is empty, allow all emails
if len(auth.Config.OAuthWhitelist) == 0 {
return true
}
// Loop through the whitelist and return true if the email matches
for _, email := range auth.Config.OAuthWhitelist {
if email == emailSrc {
return true
}
}
// If no emails match, return false
return false
}
func (auth *Auth) GetCookieStore() *sessions.CookieStore {
// Create a new cookie store
store := sessions.NewCookieStore([]byte(auth.Config.Secret))
// Configure the cookie store
store.Options = &sessions.Options{
Path: "/",
Domain: fmt.Sprintf(".%s", auth.Config.Domain),
Secure: auth.Config.CookieSecure,
MaxAge: auth.Config.SessionExpiry,
HttpOnly: true,
}
// Set the cookie store
return store
} }
func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) error { func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) error {
log.Debug().Msg("Creating session cookie") log.Debug().Msg("Creating session cookie")
// Get cookie store
store := auth.GetCookieStore()
// Get session // Get session
session, err := auth.GetSession(c) sessions, err := store.Get(c.Request, "tinyauth")
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to get session")
return err return err
} }
@@ -159,18 +101,15 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie)
} }
// Set data // Set data
session.Values["username"] = data.Username sessions.Values["username"] = data.Username
session.Values["name"] = data.Name sessions.Values["provider"] = data.Provider
session.Values["email"] = data.Email sessions.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix()
session.Values["provider"] = data.Provider sessions.Values["totpPending"] = data.TotpPending
session.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix()
session.Values["totpPending"] = data.TotpPending
session.Values["oauthGroups"] = data.OAuthGroups
// Save session // Save session
err = session.Save(c.Request, c.Writer) err = sessions.Save(c.Request, c.Writer)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to save session")
return err return err
} }
@@ -181,22 +120,25 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie)
func (auth *Auth) DeleteSessionCookie(c *gin.Context) error { func (auth *Auth) DeleteSessionCookie(c *gin.Context) error {
log.Debug().Msg("Deleting session cookie") log.Debug().Msg("Deleting session cookie")
// Get cookie store
store := auth.GetCookieStore()
// Get session // Get session
session, err := auth.GetSession(c) sessions, err := store.Get(c.Request, "tinyauth")
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to get session")
return err return err
} }
// Delete all values in the session // Clear session
for key := range session.Values { for key := range sessions.Values {
delete(session.Values, key) delete(sessions.Values, key)
} }
// Save session // Save session
err = session.Save(c.Request, c.Writer) err = sessions.Save(c.Request, c.Writer)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to save session")
return err return err
} }
@@ -207,31 +149,31 @@ func (auth *Auth) DeleteSessionCookie(c *gin.Context) error {
func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) { func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) {
log.Debug().Msg("Getting session cookie") log.Debug().Msg("Getting session cookie")
// Get cookie store
store := auth.GetCookieStore()
// Get session // Get session
session, err := auth.GetSession(c) sessions, err := store.Get(c.Request, "tinyauth")
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to get session")
return types.SessionCookie{}, err return types.SessionCookie{}, err
} }
log.Debug().Msg("Got session") // Get data
cookieUsername := sessions.Values["username"]
cookieProvider := sessions.Values["provider"]
cookieExpiry := sessions.Values["expiry"]
cookieTotpPending := sessions.Values["totpPending"]
// Get data from session // Convert interfaces to correct types
username, usernameOk := session.Values["username"].(string) username, usernameOk := cookieUsername.(string)
email, emailOk := session.Values["email"].(string) provider, providerOk := cookieProvider.(string)
name, nameOk := session.Values["name"].(string) expiry, expiryOk := cookieExpiry.(int64)
provider, providerOK := session.Values["provider"].(string) totpPending, totpPendingOk := cookieTotpPending.(bool)
expiry, expiryOk := session.Values["expiry"].(int64)
totpPending, totpPendingOk := session.Values["totpPending"].(bool)
oauthGroups, oauthGroupsOk := session.Values["oauthGroups"].(string)
if !usernameOk || !providerOK || !expiryOk || !totpPendingOk || !emailOk || !nameOk || !oauthGroupsOk { // Check if the cookie is invalid
log.Warn().Msg("Session cookie is invalid") if !usernameOk || !providerOk || !expiryOk || !totpPendingOk {
log.Warn().Msg("Session cookie invalid")
// If any data is missing, delete the session cookie
auth.DeleteSessionCookie(c)
// Return empty cookie
return types.SessionCookie{}, nil return types.SessionCookie{}, nil
} }
@@ -246,16 +188,13 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error)
return types.SessionCookie{}, nil return types.SessionCookie{}, nil
} }
log.Debug().Str("username", username).Str("provider", provider).Int64("expiry", expiry).Bool("totpPending", totpPending).Str("name", name).Str("email", email).Str("oauthGroups", oauthGroups).Msg("Parsed cookie") log.Debug().Str("username", username).Str("provider", provider).Int64("expiry", expiry).Bool("totpPending", totpPending).Msg("Parsed cookie")
// Return the cookie // Return the cookie
return types.SessionCookie{ return types.SessionCookie{
Username: username, Username: username,
Name: name,
Email: email,
Provider: provider, Provider: provider,
TotpPending: totpPending, TotpPending: totpPending,
OAuthGroups: oauthGroups,
}, nil }, nil
} }
@@ -264,52 +203,59 @@ func (auth *Auth) UserAuthConfigured() bool {
return len(auth.Config.Users) > 0 return len(auth.Config.Users) > 0
} }
func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext, labels types.TinyauthLabels) bool { 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
}
// Check if oauth is allowed // Check if oauth is allowed
if context.OAuth { if context.OAuth {
if len(labels.OAuthWhitelist) == 0 {
return true, nil
}
log.Debug().Msg("Checking OAuth whitelist") log.Debug().Msg("Checking OAuth whitelist")
return utils.CheckWhitelist(labels.OAuthWhitelist, context.Email) if slices.Contains(labels.OAuthWhitelist, context.Username) {
} return true, nil
// Check users
log.Debug().Msg("Checking users")
return utils.CheckWhitelist(labels.Users, context.Username)
}
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 // Check if user is allowed
log.Debug().Msg("No groups matched") if len(labels.Users) != 0 {
log.Debug().Msg("Checking users")
if slices.Contains(labels.Users, context.Username) {
return true, nil
}
}
// Return false // Not allowed
return false return false, nil
} }
func (auth *Auth) AuthEnabled(c *gin.Context, labels types.TinyauthLabels) (bool, error) { func (auth *Auth) AuthEnabled(c *gin.Context) (bool, error) {
// Get headers // Get headers
uri := c.Request.Header.Get("X-Forwarded-Uri") 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 // Check if the allowed label is empty
if labels.Allowed == "" { if labels.Allowed == "" {

View File

@@ -1,147 +0,0 @@
package auth_test
import (
"testing"
"time"
"tinyauth/internal/auth"
"tinyauth/internal/docker"
"tinyauth/internal/types"
)
var config = types.AuthConfig{
Users: types.Users{},
OauthWhitelist: "",
SessionExpiry: 3600,
}
func TestLoginRateLimiting(t *testing.T) {
// Initialize a new auth service with 3 max retries and 5 seconds timeout
config.LoginMaxRetries = 3
config.LoginTimeout = 5
authService := auth.NewAuth(config, &docker.Docker{})
// Test identifier
identifier := "test_user"
// Test successful login - should not lock account
t.Log("Testing successful login")
authService.RecordLoginAttempt(identifier, true)
locked, _ := authService.IsAccountLocked(identifier)
if locked {
t.Fatalf("Account should not be locked after successful login")
}
// Test 2 failed attempts - should not lock account yet
t.Log("Testing 2 failed login attempts")
authService.RecordLoginAttempt(identifier, false)
authService.RecordLoginAttempt(identifier, false)
locked, _ = authService.IsAccountLocked(identifier)
if locked {
t.Fatalf("Account should not be locked after only 2 failed attempts")
}
// Add one more failed attempt (total 3) - should lock account with maxRetries=3
t.Log("Testing 3 failed login attempts")
authService.RecordLoginAttempt(identifier, false)
locked, remainingTime := authService.IsAccountLocked(identifier)
if !locked {
t.Fatalf("Account should be locked after reaching max retries")
}
if remainingTime <= 0 || remainingTime > 5 {
t.Fatalf("Expected remaining time between 1-5 seconds, got %d", remainingTime)
}
// Test reset after waiting for timeout - use 1 second timeout for fast testing
t.Log("Testing unlocking after timeout")
// Reinitialize auth service with a shorter timeout for testing
config.LoginTimeout = 1
config.LoginMaxRetries = 3
authService = auth.NewAuth(config, &docker.Docker{})
// Add enough failed attempts to lock the account
for i := 0; i < 3; i++ {
authService.RecordLoginAttempt(identifier, false)
}
// Verify it's locked
locked, _ = authService.IsAccountLocked(identifier)
if !locked {
t.Fatalf("Account should be locked initially")
}
// Wait a bit and verify it gets unlocked after timeout
time.Sleep(1500 * time.Millisecond) // Wait longer than the timeout
locked, _ = authService.IsAccountLocked(identifier)
if locked {
t.Fatalf("Account should be unlocked after timeout period")
}
// Test disabled rate limiting
t.Log("Testing disabled rate limiting")
config.LoginMaxRetries = 0
config.LoginTimeout = 0
authService = auth.NewAuth(config, &docker.Docker{})
for i := 0; i < 10; i++ {
authService.RecordLoginAttempt(identifier, false)
}
locked, _ = authService.IsAccountLocked(identifier)
if locked {
t.Fatalf("Account should not be locked when rate limiting is disabled")
}
}
func TestConcurrentLoginAttempts(t *testing.T) {
// Initialize a new auth service with 2 max retries and 5 seconds timeout
config.LoginMaxRetries = 2
config.LoginTimeout = 5
authService := auth.NewAuth(config, &docker.Docker{})
// Test multiple identifiers
identifiers := []string{"user1", "user2", "user3"}
// Test that locking one identifier doesn't affect others
t.Log("Testing multiple identifiers")
// Add enough failed attempts to lock first user (2 attempts with maxRetries=2)
authService.RecordLoginAttempt(identifiers[0], false)
authService.RecordLoginAttempt(identifiers[0], false)
// Check if first user is locked
locked, _ := authService.IsAccountLocked(identifiers[0])
if !locked {
t.Fatalf("User1 should be locked after reaching max retries")
}
// Check that other users are not affected
for i := 1; i < len(identifiers); i++ {
locked, _ := authService.IsAccountLocked(identifiers[i])
if locked {
t.Fatalf("User%d should not be locked", i+1)
}
}
// Test successful login after failed attempts (but before lock)
t.Log("Testing successful login after failed attempts but before lock")
// One failed attempt for user2
authService.RecordLoginAttempt(identifiers[1], false)
// Successful login should reset the counter
authService.RecordLoginAttempt(identifiers[1], true)
// Now try a failed login again - should not be locked as counter was reset
authService.RecordLoginAttempt(identifiers[1], false)
locked, _ = authService.IsAccountLocked(identifiers[1])
if locked {
t.Fatalf("User2 should not be locked after successful login reset")
}
}

View File

@@ -6,13 +6,4 @@ var TinyauthLabels = []string{
"tinyauth.users", "tinyauth.users",
"tinyauth.allowed", "tinyauth.allowed",
"tinyauth.headers", "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"`
} }

View File

@@ -6,7 +6,8 @@ import (
"tinyauth/internal/types" "tinyauth/internal/types"
"tinyauth/internal/utils" "tinyauth/internal/utils"
container "github.com/docker/docker/api/types/container" apiTypes "github.com/docker/docker/api/types"
containerTypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@@ -37,9 +38,9 @@ func (docker *Docker) Init() error {
return nil return nil
} }
func (docker *Docker) GetContainers() ([]container.Summary, error) { func (docker *Docker) GetContainers() ([]apiTypes.Container, error) {
// Get the list of containers // Get the list of containers
containers, err := docker.Client.ContainerList(docker.Context, container.ListOptions{}) containers, err := docker.Client.ContainerList(docker.Context, containerTypes.ListOptions{})
// Check if there was an error // Check if there was an error
if err != nil { if err != nil {
@@ -50,13 +51,13 @@ func (docker *Docker) GetContainers() ([]container.Summary, error) {
return containers, nil return containers, nil
} }
func (docker *Docker) InspectContainer(containerId string) (container.InspectResponse, error) { func (docker *Docker) InspectContainer(containerId string) (apiTypes.ContainerJSON, error) {
// Inspect the container // Inspect the container
inspect, err := docker.Client.ContainerInspect(docker.Context, containerId) inspect, err := docker.Client.ContainerInspect(docker.Context, containerId)
// Check if there was an error // Check if there was an error
if err != nil { if err != nil {
return container.InspectResponse{}, err return apiTypes.ContainerJSON{}, err
} }
// Return the inspect // Return the inspect

View File

@@ -2,15 +2,14 @@ package handlers
import ( import (
"fmt" "fmt"
"math/rand/v2"
"net/http" "net/http"
"strings" "strings"
"time"
"tinyauth/internal/auth" "tinyauth/internal/auth"
"tinyauth/internal/docker" "tinyauth/internal/docker"
"tinyauth/internal/hooks" "tinyauth/internal/hooks"
"tinyauth/internal/providers" "tinyauth/internal/providers"
"tinyauth/internal/types" "tinyauth/internal/types"
"tinyauth/internal/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/go-querystring/query" "github.com/google/go-querystring/query"
@@ -20,20 +19,20 @@ import (
func NewHandlers(config types.HandlersConfig, auth *auth.Auth, hooks *hooks.Hooks, providers *providers.Providers, docker *docker.Docker) *Handlers { func NewHandlers(config types.HandlersConfig, auth *auth.Auth, hooks *hooks.Hooks, providers *providers.Providers, docker *docker.Docker) *Handlers {
return &Handlers{ return &Handlers{
Config: config,
Auth: auth, Auth: auth,
Hooks: hooks, Hooks: hooks,
Providers: providers, Providers: providers,
Docker: docker, Docker: docker,
Config: config,
} }
} }
type Handlers struct { type Handlers struct {
Config types.HandlersConfig
Auth *auth.Auth Auth *auth.Auth
Hooks *hooks.Hooks Hooks *hooks.Hooks
Providers *providers.Providers Providers *providers.Providers
Docker *docker.Docker Docker *docker.Docker
Config types.HandlersConfig
} }
func (h *Handlers) AuthHandler(c *gin.Context) { func (h *Handlers) AuthHandler(c *gin.Context) {
@@ -69,15 +68,12 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
proto := c.Request.Header.Get("X-Forwarded-Proto") proto := c.Request.Header.Get("X-Forwarded-Proto")
host := c.Request.Header.Get("X-Forwarded-Host") host := c.Request.Header.Get("X-Forwarded-Host")
// Get the app id // Check if auth is enabled
appId := strings.Split(host, ".")[0] authEnabled, err := h.Auth.AuthEnabled(c)
// Get the container labels
labels, err := h.Docker.GetLabels(appId)
// Check if there was an error // Check if there was an error
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to get container labels") log.Error().Err(err).Msg("Failed to check if app is allowed")
if proxy.Proxy == "nginx" || !isBrowser { if proxy.Proxy == "nginx" || !isBrowser {
c.JSON(500, gin.H{ c.JSON(500, gin.H{
@@ -91,8 +87,11 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
return return
} }
// Check if auth is enabled // Get the app id
authEnabled, err := h.Auth.AuthEnabled(c, labels) appId := strings.Split(host, ".")[0]
// Get the container labels
labels, err := h.Docker.GetLabels(appId)
// Check if there was an error // Check if there was an error
if err != nil { if err != nil {
@@ -114,7 +113,7 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
if !authEnabled { if !authEnabled {
for key, value := range labels.Headers { for key, value := range labels.Headers {
log.Debug().Str("key", key).Str("value", value).Msg("Setting header") log.Debug().Str("key", key).Str("value", value).Msg("Setting header")
c.Header(key, utils.SanitizeHeader(value)) c.Header(key, value)
} }
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"status": 200, "status": 200,
@@ -131,7 +130,23 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
log.Debug().Msg("Authenticated") log.Debug().Msg("Authenticated")
// Check if user is allowed to access subdomain, if request is nginx.example.com the subdomain (resource) is nginx // Check if user is allowed to access subdomain, if request is nginx.example.com the subdomain (resource) is nginx
appAllowed := h.Auth.ResourceAllowed(c, userContext, labels) 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
}
log.Debug().Bool("appAllowed", appAllowed).Msg("Checking if app is allowed") log.Debug().Bool("appAllowed", appAllowed).Msg("Checking if app is allowed")
@@ -150,20 +165,11 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
return return
} }
// Values
values := types.UnauthorizedQuery{
Resource: strings.Split(host, ".")[0],
}
// Use either username or email
if userContext.OAuth {
values.Username = userContext.Email
} else {
values.Username = userContext.Username
}
// Build query // Build query
queries, err := query.Values(values) queries, err := query.Values(types.UnauthorizedQuery{
Username: userContext.Username,
Resource: strings.Split(host, ".")[0],
})
// Handle error (no need to check for nginx/headers since we are sure we are using caddy/traefik) // Handle error (no need to check for nginx/headers since we are sure we are using caddy/traefik)
if err != nil { if err != nil {
@@ -177,65 +183,13 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
return return
} }
log.Debug().Interface("labels", labels).Msg("Got labels") // Set the user header
c.Header("Remote-User", userContext.Username)
// 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 // Set the rest of the headers
for key, value := range labels.Headers { for key, value := range labels.Headers {
log.Debug().Str("key", key).Str("value", value).Msg("Setting header") log.Debug().Str("key", key).Str("value", value).Msg("Setting header")
c.Header(key, utils.SanitizeHeader(value)) c.Header(key, value)
} }
// The user is allowed to access the app // The user is allowed to access the app
@@ -295,34 +249,12 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
log.Debug().Msg("Got login request") log.Debug().Msg("Got login request")
// Get client IP for rate limiting
clientIP := c.ClientIP()
// Create an identifier for rate limiting (username or IP if username doesn't exist yet)
rateIdentifier := login.Username
if rateIdentifier == "" {
rateIdentifier = clientIP
}
// Check if the account is locked due to too many failed attempts
locked, remainingTime := h.Auth.IsAccountLocked(rateIdentifier)
if locked {
log.Warn().Str("identifier", rateIdentifier).Int("remaining_seconds", remainingTime).Msg("Account is locked due to too many failed login attempts")
c.JSON(429, gin.H{
"status": 429,
"message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remainingTime),
})
return
}
// Get user based on username // Get user based on username
user := h.Auth.GetUser(login.Username) user := h.Auth.GetUser(login.Username)
// User does not exist // User does not exist
if user == nil { if user == nil {
log.Debug().Str("username", login.Username).Msg("User not found") log.Debug().Str("username", login.Username).Msg("User not found")
// Record failed login attempt
h.Auth.RecordLoginAttempt(rateIdentifier, false)
c.JSON(401, gin.H{ c.JSON(401, gin.H{
"status": 401, "status": 401,
"message": "Unauthorized", "message": "Unauthorized",
@@ -335,8 +267,6 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
// Check if password is correct // Check if password is correct
if !h.Auth.CheckPassword(*user, login.Password) { if !h.Auth.CheckPassword(*user, login.Password) {
log.Debug().Str("username", login.Username).Msg("Password incorrect") log.Debug().Str("username", login.Username).Msg("Password incorrect")
// Record failed login attempt
h.Auth.RecordLoginAttempt(rateIdentifier, false)
c.JSON(401, gin.H{ c.JSON(401, gin.H{
"status": 401, "status": 401,
"message": "Unauthorized", "message": "Unauthorized",
@@ -346,9 +276,6 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
log.Debug().Msg("Password correct, checking totp") log.Debug().Msg("Password correct, checking totp")
// Record successful login attempt (will reset failed attempt counter)
h.Auth.RecordLoginAttempt(rateIdentifier, true)
// Check if user has totp enabled // Check if user has totp enabled
if user.TotpSecret != "" { if user.TotpSecret != "" {
log.Debug().Msg("Totp enabled") log.Debug().Msg("Totp enabled")
@@ -356,8 +283,6 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
// Set totp pending cookie // Set totp pending cookie
h.Auth.CreateSessionCookie(c, &types.SessionCookie{ h.Auth.CreateSessionCookie(c, &types.SessionCookie{
Username: login.Username, Username: login.Username,
Name: utils.Capitalize(login.Username),
Email: fmt.Sprintf("%s@%s", strings.ToLower(login.Username), h.Config.Domain),
Provider: "username", Provider: "username",
TotpPending: true, TotpPending: true,
}) })
@@ -376,8 +301,6 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
// Create session cookie with username as provider // Create session cookie with username as provider
h.Auth.CreateSessionCookie(c, &types.SessionCookie{ h.Auth.CreateSessionCookie(c, &types.SessionCookie{
Username: login.Username, Username: login.Username,
Name: utils.Capitalize(login.Username),
Email: fmt.Sprintf("%s@%s", strings.ToLower(login.Username), h.Config.Domain),
Provider: "username", Provider: "username",
}) })
@@ -452,8 +375,6 @@ func (h *Handlers) TotpHandler(c *gin.Context) {
// Create session cookie with username as provider // Create session cookie with username as provider
h.Auth.CreateSessionCookie(c, &types.SessionCookie{ h.Auth.CreateSessionCookie(c, &types.SessionCookie{
Username: user.Username, Username: user.Username,
Name: utils.Capitalize(user.Username),
Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), h.Config.Domain),
Provider: "username", Provider: "username",
}) })
@@ -472,6 +393,9 @@ func (h *Handlers) LogoutHandler(c *gin.Context) {
log.Debug().Msg("Cleaning up redirect cookie") log.Debug().Msg("Cleaning up redirect cookie")
// Clean up redirect cookie if it exists
c.SetCookie("tinyauth_redirect_uri", "", -1, "/", h.Config.Domain, h.Config.CookieSecure, true)
// Return logged out // Return logged out
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"status": 200, "status": 200,
@@ -492,14 +416,12 @@ func (h *Handlers) AppHandler(c *gin.Context) {
// Create app context struct // Create app context struct
appContext := types.AppContext{ appContext := types.AppContext{
Status: 200, Status: 200,
Message: "OK", Message: "OK",
ConfiguredProviders: configuredProviders, ConfiguredProviders: configuredProviders,
DisableContinue: h.Config.DisableContinue, DisableContinue: h.Config.DisableContinue,
Title: h.Config.Title, Title: h.Config.Title,
GenericName: h.Config.GenericName, GenericName: h.Config.GenericName,
Domain: h.Config.Domain,
ForgotPasswordMessage: h.Config.ForgotPasswordMessage,
} }
// Return app context // Return app context
@@ -517,8 +439,6 @@ func (h *Handlers) UserHandler(c *gin.Context) {
Status: 200, Status: 200,
IsLoggedIn: userContext.IsLoggedIn, IsLoggedIn: userContext.IsLoggedIn,
Username: userContext.Username, Username: userContext.Username,
Name: userContext.Name,
Email: userContext.Email,
Provider: userContext.Provider, Provider: userContext.Provider,
Oauth: userContext.OAuth, Oauth: userContext.OAuth,
TotpPending: userContext.TotpPending, TotpPending: userContext.TotpPending,
@@ -571,24 +491,44 @@ func (h *Handlers) OauthUrlHandler(c *gin.Context) {
log.Debug().Str("provider", request.Provider).Msg("Got provider") log.Debug().Str("provider", request.Provider).Msg("Got provider")
// Create state
state := provider.GenerateState()
// Get auth URL // Get auth URL
authURL := provider.GetAuthURL(state) authURL := provider.GetAuthURL()
log.Debug().Msg("Got auth URL") log.Debug().Msg("Got auth URL")
// Set CSRF cookie
c.SetCookie("tinyauth-csrf", state, int(time.Hour.Seconds()), "/", "", h.Config.CookieSecure, true)
// Get redirect URI // Get redirect URI
redirectURI := c.Query("redirect_uri") redirectURI := c.Query("redirect_uri")
// Set redirect cookie if redirect URI is provided // Set redirect cookie if redirect URI is provided
if redirectURI != "" { if redirectURI != "" {
log.Debug().Str("redirectURI", redirectURI).Msg("Setting redirect cookie") log.Debug().Str("redirectURI", redirectURI).Msg("Setting redirect cookie")
c.SetCookie("tinyauth-redirect", redirectURI, int(time.Hour.Seconds()), "/", "", h.Config.CookieSecure, true) c.SetCookie("tinyauth_redirect_uri", redirectURI, 3600, "/", h.Config.Domain, h.Config.CookieSecure, true)
}
// Tailscale does not have an auth url so we create a random code (does not need to be secure) to avoid caching and send it
if request.Provider == "tailscale" {
// Build tailscale query
queries, err := query.Values(types.TailscaleQuery{
Code: (1000 + rand.IntN(9000)),
})
// Handle error
if err != nil {
log.Error().Err(err).Msg("Failed to build queries")
c.JSON(500, gin.H{
"status": 500,
"message": "Internal Server Error",
})
return
}
// Return tailscale URL (immidiately redirects to the callback)
c.JSON(200, gin.H{
"status": 200,
"message": "OK",
"url": fmt.Sprintf("%s/api/oauth/callback/tailscale?%s", h.Config.AppURL, queries.Encode()),
})
return
} }
// Return auth URL // Return auth URL
@@ -615,33 +555,16 @@ func (h *Handlers) OauthCallbackHandler(c *gin.Context) {
log.Debug().Interface("provider", providerName.Provider).Msg("Got provider name") log.Debug().Interface("provider", providerName.Provider).Msg("Got provider name")
// Get state
state := c.Query("state")
// Get CSRF cookie
csrfCookie, err := c.Cookie("tinyauth-csrf")
if err != nil {
log.Debug().Msg("No CSRF cookie")
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
return
}
log.Debug().Str("csrfCookie", csrfCookie).Msg("Got CSRF cookie")
// Check if CSRF cookie is valid
if csrfCookie != state {
log.Warn().Msg("Invalid CSRF cookie or CSRF cookie does not match with the state")
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
return
}
// Clean up CSRF cookie
c.SetCookie("tinyauth-csrf", "", -1, "/", "", h.Config.CookieSecure, true)
// Get code // Get code
code := c.Query("code") code := c.Query("code")
// Code empty so redirect to error
if code == "" {
log.Error().Msg("No code provided")
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
return
}
log.Debug().Msg("Got code") log.Debug().Msg("Got code")
// Get provider // Get provider
@@ -662,42 +585,35 @@ func (h *Handlers) OauthCallbackHandler(c *gin.Context) {
// Handle error // Handle error
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to exchange token") log.Error().Msg("Failed to exchange token")
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
return return
} }
// Get user // Get email
user, err := h.Providers.GetUser(providerName.Provider) email, err := h.Providers.GetUser(providerName.Provider)
log.Debug().Str("email", email).Msg("Got email")
// Handle error // Handle error
if err != nil { if err != nil {
log.Error().Msg("Failed to get user") log.Error().Msg("Failed to get email")
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)) c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
return return
} }
// Email is not whitelisted // Email is not whitelisted
if !h.Auth.EmailWhitelisted(user.Email) { if !h.Auth.EmailWhitelisted(email) {
log.Warn().Str("email", user.Email).Msg("Email not whitelisted") log.Warn().Str("email", email).Msg("Email not whitelisted")
// Build query // Build query
queries, err := query.Values(types.UnauthorizedQuery{ queries, err := query.Values(types.UnauthorizedQuery{
Username: user.Email, Username: email,
}) })
// Handle error // Handle error
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to build queries") log.Error().Msg("Failed to build queries")
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
return return
} }
@@ -708,61 +624,39 @@ func (h *Handlers) OauthCallbackHandler(c *gin.Context) {
log.Debug().Msg("Email whitelisted") log.Debug().Msg("Email whitelisted")
// Get username // Create session cookie
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{ h.Auth.CreateSessionCookie(c, &types.SessionCookie{
Username: username, Username: email,
Name: name, Provider: providerName.Provider,
Email: user.Email,
Provider: providerName.Provider,
OAuthGroups: strings.Join(user.Groups, ","),
}) })
// Check if we have a redirect URI // Get redirect URI
redirectCookie, err := c.Cookie("tinyauth-redirect") redirectURI, err := c.Cookie("tinyauth_redirect_uri")
// If it is empty it means that no redirect_uri was provided to the login screen so we just log in
if err != nil { if err != nil {
log.Debug().Msg("No redirect cookie")
c.Redirect(http.StatusPermanentRedirect, h.Config.AppURL) c.Redirect(http.StatusPermanentRedirect, h.Config.AppURL)
return
} }
log.Debug().Str("redirectURI", redirectCookie).Msg("Got redirect URI") log.Debug().Str("redirectURI", redirectURI).Msg("Got redirect URI")
// Clean up redirect cookie since we already have the value
c.SetCookie("tinyauth_redirect_uri", "", -1, "/", h.Config.Domain, h.Config.CookieSecure, true)
// Build query // Build query
queries, err := query.Values(types.LoginQuery{ queries, err := query.Values(types.LoginQuery{
RedirectURI: redirectCookie, RedirectURI: redirectURI,
}) })
log.Debug().Msg("Got redirect query") log.Debug().Msg("Got redirect query")
// Handle error // Handle error
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to build queries") log.Error().Msg("Failed to build queries")
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
return return
} }
// Clean up redirect cookie
c.SetCookie("tinyauth-redirect", "", -1, "/", "", h.Config.CookieSecure, true)
// Redirect to continue with the redirect URI // Redirect to continue with the redirect URI
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/continue?%s", h.Config.AppURL, queries.Encode())) c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/continue?%s", h.Config.AppURL, queries.Encode()))
} }

View File

@@ -1,27 +1,22 @@
package hooks package hooks
import ( import (
"fmt"
"strings"
"tinyauth/internal/auth" "tinyauth/internal/auth"
"tinyauth/internal/providers" "tinyauth/internal/providers"
"tinyauth/internal/types" "tinyauth/internal/types"
"tinyauth/internal/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
func NewHooks(config types.HooksConfig, auth *auth.Auth, providers *providers.Providers) *Hooks { func NewHooks(auth *auth.Auth, providers *providers.Providers) *Hooks {
return &Hooks{ return &Hooks{
Config: config,
Auth: auth, Auth: auth,
Providers: providers, Providers: providers,
} }
} }
type Hooks struct { type Hooks struct {
Config types.HooksConfig
Auth *auth.Auth Auth *auth.Auth
Providers *providers.Providers Providers *providers.Providers
} }
@@ -29,6 +24,12 @@ type Hooks struct {
func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
// Get session cookie and basic auth // Get session cookie and basic auth
cookie, err := hooks.Auth.GetSessionCookie(c) cookie, err := hooks.Auth.GetSessionCookie(c)
if err != nil {
log.Error().Err(err).Msg("Failed to get session cookie")
return types.UserContext{}
}
basic := hooks.Auth.GetBasicAuth(c) basic := hooks.Auth.GetBasicAuth(c)
// Check if basic auth is set // Check if basic auth is set
@@ -41,31 +42,24 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
if user != nil && hooks.Auth.CheckPassword(*user, basic.Password) { if user != nil && hooks.Auth.CheckPassword(*user, basic.Password) {
// Return user context since we are logged in with basic auth // Return user context since we are logged in with basic auth
return types.UserContext{ return types.UserContext{
Username: basic.Username, Username: basic.Username,
Name: utils.Capitalize(basic.Username), IsLoggedIn: true,
Email: fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), hooks.Config.Domain), OAuth: false,
IsLoggedIn: true, Provider: "basic",
Provider: "basic", TotpPending: false,
} }
} }
} }
// Check cookie error after basic auth
if err != nil {
log.Error().Err(err).Msg("Failed to get session cookie")
// Return empty context
return types.UserContext{}
}
// Check if session cookie has totp pending // Check if session cookie has totp pending
if cookie.TotpPending { if cookie.TotpPending {
log.Debug().Msg("Totp pending") log.Debug().Msg("Totp pending")
// Return empty context since we are pending totp // Return empty context since we are pending totp
return types.UserContext{ return types.UserContext{
Username: cookie.Username, Username: cookie.Username,
Name: cookie.Name, IsLoggedIn: false,
Email: cookie.Email, OAuth: false,
Provider: cookie.Provider, Provider: cookie.Provider,
TotpPending: true, TotpPending: true,
} }
@@ -81,11 +75,11 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
// It exists so we are logged in // It exists so we are logged in
return types.UserContext{ return types.UserContext{
Username: cookie.Username, Username: cookie.Username,
Name: cookie.Name, IsLoggedIn: true,
Email: cookie.Email, OAuth: false,
IsLoggedIn: true, Provider: "username",
Provider: "username", TotpPending: false,
} }
} }
} }
@@ -107,7 +101,13 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
hooks.Auth.DeleteSessionCookie(c) hooks.Auth.DeleteSessionCookie(c)
// Return empty context // Return empty context
return types.UserContext{} return types.UserContext{
Username: "",
IsLoggedIn: false,
OAuth: false,
Provider: "",
TotpPending: false,
}
} }
log.Debug().Msg("Email is whitelisted") log.Debug().Msg("Email is whitelisted")
@@ -115,15 +115,19 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
// Return user context since we are logged in with oauth // Return user context since we are logged in with oauth
return types.UserContext{ return types.UserContext{
Username: cookie.Username, Username: cookie.Username,
Name: cookie.Name,
Email: cookie.Email,
IsLoggedIn: true, IsLoggedIn: true,
OAuth: true, OAuth: true,
Provider: cookie.Provider, Provider: cookie.Provider,
OAuthGroups: cookie.OAuthGroups, TotpPending: false,
} }
} }
// Neither basic auth or oauth is set so we return an empty context // Neither basic auth or oauth is set so we return an empty context
return types.UserContext{} return types.UserContext{
Username: "",
IsLoggedIn: false,
OAuth: false,
Provider: "",
TotpPending: false,
}
} }

View File

@@ -2,8 +2,6 @@ package oauth
import ( import (
"context" "context"
"crypto/rand"
"encoding/base64"
"net/http" "net/http"
"golang.org/x/oauth2" "golang.org/x/oauth2"
@@ -16,10 +14,10 @@ func NewOAuth(config oauth2.Config) *OAuth {
} }
type OAuth struct { type OAuth struct {
Config oauth2.Config Verifier string
Context context.Context Context context.Context
Token *oauth2.Token Token *oauth2.Token
Verifier string Config oauth2.Config
} }
func (oauth *OAuth) Init() { func (oauth *OAuth) Init() {
@@ -28,9 +26,9 @@ func (oauth *OAuth) Init() {
oauth.Verifier = oauth2.GenerateVerifier() oauth.Verifier = oauth2.GenerateVerifier()
} }
func (oauth *OAuth) GetAuthURL(state string) string { func (oauth *OAuth) GetAuthURL() string {
// Return the auth url // Return the auth url
return oauth.Config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(oauth.Verifier)) return oauth.Config.AuthCodeURL("state", oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(oauth.Verifier))
} }
func (oauth *OAuth) ExchangeToken(code string) (string, error) { func (oauth *OAuth) ExchangeToken(code string) (string, error) {
@@ -53,16 +51,3 @@ func (oauth *OAuth) GetClient() *http.Client {
// Return the http client with the token set // Return the http client with the token set
return oauth.Config.Client(oauth.Context, oauth.Token) return oauth.Config.Client(oauth.Context, oauth.Token)
} }
func (oauth *OAuth) GenerateState() string {
// Generate a random state string
b := make([]byte, 128)
// Fill the byte slice with random data
rand.Read(b)
// Encode the byte slice to a base64 string
state := base64.URLEncoding.EncodeToString(b)
return state
}

View File

@@ -4,25 +4,24 @@ import (
"encoding/json" "encoding/json"
"io" "io"
"net/http" "net/http"
"tinyauth/internal/constants"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
func GetGenericUser(client *http.Client, url string) (constants.Claims, error) { // We are assuming that the generic provider will return a JSON object with an email field
// Create user struct type GenericUserInfoResponse struct {
var user constants.Claims Email string `json:"email"`
}
func GetGenericEmail(client *http.Client, url string) (string, error) {
// Using the oauth client get the user info url // Using the oauth client get the user info url
res, err := client.Get(url) res, err := client.Get(url)
// Check if there was an error // Check if there was an error
if err != nil { if err != nil {
return user, err return "", err
} }
defer res.Body.Close()
log.Debug().Msg("Got response from generic provider") log.Debug().Msg("Got response from generic provider")
// Read the body of the response // Read the body of the response
@@ -30,21 +29,24 @@ func GetGenericUser(client *http.Client, url string) (constants.Claims, error) {
// Check if there was an error // Check if there was an error
if err != nil { if err != nil {
return user, err return "", err
} }
log.Debug().Msg("Read body from generic provider") 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 // Unmarshal the body into the user struct
err = json.Unmarshal(body, &user) err = json.Unmarshal(body, &user)
// Check if there was an error // Check if there was an error
if err != nil { if err != nil {
return user, err return "", err
} }
log.Debug().Msg("Parsed user from generic provider") log.Debug().Msg("Parsed user from generic provider")
// Return the user // Return the email
return user, nil return user.Email, nil
} }

View File

@@ -5,96 +5,51 @@ import (
"errors" "errors"
"io" "io"
"net/http" "net/http"
"tinyauth/internal/constants"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
// Response for the github email endpoint // Github has a different response than the generic provider
type GithubEmailResponse []struct { type GithubUserInfoResponse []struct {
Email string `json:"email"` Email string `json:"email"`
Primary bool `json:"primary"` Primary bool `json:"primary"`
} }
// Response for the github user endpoint
type GithubUserInfoResponse struct {
Login string `json:"login"`
Name string `json:"name"`
}
// The scopes required for the github provider // The scopes required for the github provider
func GithubScopes() []string { func GithubScopes() []string {
return []string{"user:email", "read:user"} return []string{"user:email"}
} }
func GetGithubUser(client *http.Client) (constants.Claims, error) { func GetGithubEmail(client *http.Client) (string, error) {
// Create user struct // Get the user emails from github using the oauth http client
var user constants.Claims res, err := client.Get("https://api.github.com/user/emails")
// 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 // Check if there was an error
if err != nil { if err != nil {
return user, err return "", err
} }
defer res.Body.Close() log.Debug().Msg("Got response from github")
log.Debug().Msg("Got user response from github")
// Read the body of the response // Read the body of the response
body, err := io.ReadAll(res.Body) body, err := io.ReadAll(res.Body)
// Check if there was an error // Check if there was an error
if err != nil { if err != nil {
return user, err return "", err
} }
log.Debug().Msg("Read user body from github") log.Debug().Msg("Read body from github")
// Parse the body into a user struct // Parse the body into a user struct
var userInfo GithubUserInfoResponse var emails 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 // Unmarshal the body into the user struct
err = json.Unmarshal(body, &emails) err = json.Unmarshal(body, &emails)
// Check if there was an error // Check if there was an error
if err != nil { if err != nil {
return user, err return "", err
} }
log.Debug().Msg("Parsed emails from github") log.Debug().Msg("Parsed emails from github")
@@ -102,26 +57,10 @@ func GetGithubUser(client *http.Client) (constants.Claims, error) {
// Find and return the primary email // Find and return the primary email
for _, email := range emails { for _, email := range emails {
if email.Primary { if email.Primary {
// Set the email then exit return email.Email, nil
user.Email = email.Email
break
} }
} }
// If no primary email was found, use the first available email // User does not have a primary email?
if len(emails) == 0 { return "", errors.New("no primary email found")
return user, errors.New("no emails found")
}
// Set the email if it is not set picking the first one
if user.Email == "" {
user.Email = emails[0].Email
}
// Set the username and name
user.PreferredUsername = userInfo.Login
user.Name = userInfo.Name
// Return
return user, nil
} }

View File

@@ -4,37 +4,29 @@ import (
"encoding/json" "encoding/json"
"io" "io"
"net/http" "net/http"
"strings"
"tinyauth/internal/constants"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
// Response for the google user endpoint // Google works the same as the generic provider
type GoogleUserInfoResponse struct { type GoogleUserInfoResponse struct {
Email string `json:"email"` Email string `json:"email"`
Name string `json:"name"`
} }
// The scopes required for the google provider // The scopes required for the google provider
func GoogleScopes() []string { func GoogleScopes() []string {
return []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"} return []string{"https://www.googleapis.com/auth/userinfo.email"}
} }
func GetGoogleUser(client *http.Client) (constants.Claims, error) { func GetGoogleEmail(client *http.Client) (string, error) {
// Create user struct
var user constants.Claims
// Get the user info from google using the oauth http client // Get the user info from google using the oauth http client
res, err := client.Get("https://www.googleapis.com/userinfo/v2/me") res, err := client.Get("https://www.googleapis.com/userinfo/v2/me")
// Check if there was an error // Check if there was an error
if err != nil { if err != nil {
return user, err return "", err
} }
defer res.Body.Close()
log.Debug().Msg("Got response from google") log.Debug().Msg("Got response from google")
// Read the body of the response // Read the body of the response
@@ -42,29 +34,24 @@ func GetGoogleUser(client *http.Client) (constants.Claims, error) {
// Check if there was an error // Check if there was an error
if err != nil { if err != nil {
return user, err return "", err
} }
log.Debug().Msg("Read body from google") log.Debug().Msg("Read body from google")
// Create a new user info struct // Parse the body into a user struct
var userInfo GoogleUserInfoResponse var user GoogleUserInfoResponse
// Unmarshal the body into the user struct // Unmarshal the body into the user struct
err = json.Unmarshal(body, &userInfo) err = json.Unmarshal(body, &user)
// Check if there was an error // Check if there was an error
if err != nil { if err != nil {
return user, err return "", err
} }
log.Debug().Msg("Parsed user from google") log.Debug().Msg("Parsed user from google")
// Map the user info to the user struct // Return the email
user.PreferredUsername = strings.Split(userInfo.Email, "@")[0] return user.Email, nil
user.Name = userInfo.Name
user.Email = userInfo.Email
// Return the user
return user, nil
} }

View File

@@ -2,7 +2,6 @@ package providers
import ( import (
"fmt" "fmt"
"tinyauth/internal/constants"
"tinyauth/internal/oauth" "tinyauth/internal/oauth"
"tinyauth/internal/types" "tinyauth/internal/types"
@@ -18,10 +17,11 @@ func NewProviders(config types.OAuthConfig) *Providers {
} }
type Providers struct { type Providers struct {
Config types.OAuthConfig Github *oauth.OAuth
Github *oauth.OAuth Google *oauth.OAuth
Google *oauth.OAuth Tailscale *oauth.OAuth
Generic *oauth.OAuth Generic *oauth.OAuth
Config types.OAuthConfig
} }
func (providers *Providers) Init() { func (providers *Providers) Init() {
@@ -59,6 +59,22 @@ func (providers *Providers) Init() {
providers.Google.Init() providers.Google.Init()
} }
if providers.Config.TailscaleClientId != "" && providers.Config.TailscaleClientSecret != "" {
log.Info().Msg("Initializing Tailscale OAuth")
// Create a new oauth provider with the tailscale config
providers.Tailscale = oauth.NewOAuth(oauth2.Config{
ClientID: providers.Config.TailscaleClientId,
ClientSecret: providers.Config.TailscaleClientSecret,
RedirectURL: fmt.Sprintf("%s/api/oauth/callback/tailscale", providers.Config.AppURL),
Scopes: TailscaleScopes(),
Endpoint: TailscaleEndpoint,
})
// Initialize the oauth provider
providers.Tailscale.Init()
}
// If we have a client id and secret for generic oauth, initialize the oauth provider // If we have a client id and secret for generic oauth, initialize the oauth provider
if providers.Config.GenericClientId != "" && providers.Config.GenericClientSecret != "" { if providers.Config.GenericClientId != "" && providers.Config.GenericClientSecret != "" {
log.Info().Msg("Initializing Generic OAuth") log.Info().Msg("Initializing Generic OAuth")
@@ -87,6 +103,8 @@ func (providers *Providers) GetProvider(provider string) *oauth.OAuth {
return providers.Github return providers.Github
case "google": case "google":
return providers.Google return providers.Google
case "tailscale":
return providers.Tailscale
case "generic": case "generic":
return providers.Generic return providers.Generic
default: default:
@@ -94,17 +112,14 @@ func (providers *Providers) GetProvider(provider string) *oauth.OAuth {
} }
} }
func (providers *Providers) GetUser(provider string) (constants.Claims, error) { func (providers *Providers) GetUser(provider string) (string, error) {
// Create user struct // Get the email from the provider
var user constants.Claims
// Get the user from the provider
switch provider { switch provider {
case "github": case "github":
// If the github provider is not configured, return an error // If the github provider is not configured, return an error
if providers.Github == nil { if providers.Github == nil {
log.Debug().Msg("Github provider not configured") log.Debug().Msg("Github provider not configured")
return user, nil return "", nil
} }
// Get the client from the github provider // Get the client from the github provider
@@ -112,23 +127,23 @@ func (providers *Providers) GetUser(provider string) (constants.Claims, error) {
log.Debug().Msg("Got client from github") log.Debug().Msg("Got client from github")
// Get the user from the github provider // Get the email from the github provider
user, err := GetGithubUser(client) email, err := GetGithubEmail(client)
// Check if there was an error // Check if there was an error
if err != nil { if err != nil {
return user, err return "", err
} }
log.Debug().Msg("Got user from github") log.Debug().Msg("Got email from github")
// Return the user // Return the email
return user, nil return email, nil
case "google": case "google":
// If the google provider is not configured, return an error // If the google provider is not configured, return an error
if providers.Google == nil { if providers.Google == nil {
log.Debug().Msg("Google provider not configured") log.Debug().Msg("Google provider not configured")
return user, nil return "", nil
} }
// Get the client from the google provider // Get the client from the google provider
@@ -136,23 +151,47 @@ func (providers *Providers) GetUser(provider string) (constants.Claims, error) {
log.Debug().Msg("Got client from google") log.Debug().Msg("Got client from google")
// Get the user from the google provider // Get the email from the google provider
user, err := GetGoogleUser(client) email, err := GetGoogleEmail(client)
// Check if there was an error // Check if there was an error
if err != nil { if err != nil {
return user, err return "", err
} }
log.Debug().Msg("Got user from google") log.Debug().Msg("Got email from google")
// Return the user // Return the email
return user, nil return email, nil
case "tailscale":
// If the tailscale provider is not configured, return an error
if providers.Tailscale == nil {
log.Debug().Msg("Tailscale provider not configured")
return "", nil
}
// Get the client from the tailscale provider
client := providers.Tailscale.GetClient()
log.Debug().Msg("Got client from tailscale")
// Get the email from the tailscale provider
email, err := GetTailscaleEmail(client)
// Check if there was an error
if err != nil {
return "", err
}
log.Debug().Msg("Got email from tailscale")
// Return the email
return email, nil
case "generic": case "generic":
// If the generic provider is not configured, return an error // If the generic provider is not configured, return an error
if providers.Generic == nil { if providers.Generic == nil {
log.Debug().Msg("Generic provider not configured") log.Debug().Msg("Generic provider not configured")
return user, nil return "", nil
} }
// Get the client from the generic provider // Get the client from the generic provider
@@ -160,20 +199,20 @@ func (providers *Providers) GetUser(provider string) (constants.Claims, error) {
log.Debug().Msg("Got client from generic") log.Debug().Msg("Got client from generic")
// Get the user from the generic provider // Get the email from the generic provider
user, err := GetGenericUser(client, providers.Config.GenericUserURL) email, err := GetGenericEmail(client, providers.Config.GenericUserURL)
// Check if there was an error // Check if there was an error
if err != nil { if err != nil {
return user, err return "", err
} }
log.Debug().Msg("Got user from generic") log.Debug().Msg("Got email from generic")
// Return the email // Return the email
return user, nil return email, nil
default: default:
return user, nil return "", nil
} }
} }
@@ -186,6 +225,9 @@ func (provider *Providers) GetConfiguredProviders() []string {
if provider.Google != nil { if provider.Google != nil {
providers = append(providers, "google") providers = append(providers, "google")
} }
if provider.Tailscale != nil {
providers = append(providers, "tailscale")
}
if provider.Generic != nil { if provider.Generic != nil {
providers = append(providers, "generic") providers = append(providers, "generic")
} }

View File

@@ -0,0 +1,68 @@
package providers
import (
"encoding/json"
"io"
"net/http"
"github.com/rs/zerolog/log"
"golang.org/x/oauth2"
)
// The tailscale email is the loginName
type TailscaleUser struct {
LoginName string `json:"loginName"`
}
// The response from the tailscale user info endpoint
type TailscaleUserInfoResponse struct {
Users []TailscaleUser `json:"users"`
}
// The scopes required for the tailscale provider
func TailscaleScopes() []string {
return []string{"users:read"}
}
// The tailscale endpoint
var TailscaleEndpoint = oauth2.Endpoint{
TokenURL: "https://api.tailscale.com/api/v2/oauth/token",
}
func GetTailscaleEmail(client *http.Client) (string, error) {
// Get the user info from tailscale using the oauth http client
res, err := client.Get("https://api.tailscale.com/api/v2/tailnet/-/users")
// Check if there was an error
if err != nil {
return "", err
}
log.Debug().Msg("Got response from tailscale")
// Read the body of the response
body, err := io.ReadAll(res.Body)
// Check if there was an error
if err != nil {
return "", err
}
log.Debug().Msg("Read body from tailscale")
// Parse the body into a user struct
var users TailscaleUserInfoResponse
// Unmarshal the body into the user struct
err = json.Unmarshal(body, &users)
// Check if there was an error
if err != nil {
return "", err
}
log.Debug().Msg("Parsed users from tailscale")
// Return the email of the first user
return users.Users[0].LoginName, nil
}

View File

@@ -20,7 +20,11 @@ type OAuthRequest struct {
type UnauthorizedQuery struct { type UnauthorizedQuery struct {
Username string `url:"username"` Username string `url:"username"`
Resource string `url:"resource"` Resource string `url:"resource"`
GroupErr bool `url:"groupErr"` }
// TailscaleQuery is the query parameters for the tailscale endpoint
type TailscaleQuery struct {
Code int `url:"code"`
} }
// Proxy is the uri parameters for the proxy endpoint // Proxy is the uri parameters for the proxy endpoint
@@ -34,25 +38,11 @@ type UserContextResponse struct {
Message string `json:"message"` Message string `json:"message"`
IsLoggedIn bool `json:"isLoggedIn"` IsLoggedIn bool `json:"isLoggedIn"`
Username string `json:"username"` Username string `json:"username"`
Name string `json:"name"`
Email string `json:"email"`
Provider string `json:"provider"` Provider string `json:"provider"`
Oauth bool `json:"oauth"` Oauth bool `json:"oauth"`
TotpPending bool `json:"totpPending"` TotpPending bool `json:"totpPending"`
} }
// App Context is the response for the app context endpoint
type AppContext struct {
Status int `json:"status"`
Message string `json:"message"`
ConfiguredProviders []string `json:"configuredProviders"`
DisableContinue bool `json:"disableContinue"`
Title string `json:"title"`
GenericName string `json:"genericName"`
Domain string `json:"domain"`
ForgotPasswordMessage string `json:"forgotPasswordMessage"`
}
// Totp request is the request for the totp endpoint // Totp request is the request for the totp endpoint
type TotpRequest struct { type TotpRequest struct {
Code string `json:"code"` Code string `json:"code"`

View File

@@ -2,63 +2,37 @@ package types
// Config is the configuration for the tinyauth server // Config is the configuration for the tinyauth server
type Config struct { type Config struct {
Port int `mapstructure:"port" validate:"required"` Port int `mapstructure:"port" validate:"required"`
Address string `validate:"required,ip4_addr" mapstructure:"address"` Address string `validate:"required,ip4_addr" mapstructure:"address"`
Secret string `validate:"required,len=32" mapstructure:"secret"` Secret string `validate:"required,len=32" mapstructure:"secret"`
SecretFile string `mapstructure:"secret-file"` SecretFile string `mapstructure:"secret-file"`
AppURL string `validate:"required,url" mapstructure:"app-url"` AppURL string `validate:"required,url" mapstructure:"app-url"`
Users string `mapstructure:"users"` Users string `mapstructure:"users"`
UsersFile string `mapstructure:"users-file"` UsersFile string `mapstructure:"users-file"`
CookieSecure bool `mapstructure:"cookie-secure"` CookieSecure bool `mapstructure:"cookie-secure"`
GithubClientId string `mapstructure:"github-client-id"` GithubClientId string `mapstructure:"github-client-id"`
GithubClientSecret string `mapstructure:"github-client-secret"` GithubClientSecret string `mapstructure:"github-client-secret"`
GithubClientSecretFile string `mapstructure:"github-client-secret-file"` GithubClientSecretFile string `mapstructure:"github-client-secret-file"`
GoogleClientId string `mapstructure:"google-client-id"` GoogleClientId string `mapstructure:"google-client-id"`
GoogleClientSecret string `mapstructure:"google-client-secret"` GoogleClientSecret string `mapstructure:"google-client-secret"`
GoogleClientSecretFile string `mapstructure:"google-client-secret-file"` GoogleClientSecretFile string `mapstructure:"google-client-secret-file"`
GenericClientId string `mapstructure:"generic-client-id"` TailscaleClientId string `mapstructure:"tailscale-client-id"`
GenericClientSecret string `mapstructure:"generic-client-secret"` TailscaleClientSecret string `mapstructure:"tailscale-client-secret"`
GenericClientSecretFile string `mapstructure:"generic-client-secret-file"` TailscaleClientSecretFile string `mapstructure:"tailscale-client-secret-file"`
GenericScopes string `mapstructure:"generic-scopes"` GenericClientId string `mapstructure:"generic-client-id"`
GenericAuthURL string `mapstructure:"generic-auth-url"` GenericClientSecret string `mapstructure:"generic-client-secret"`
GenericTokenURL string `mapstructure:"generic-token-url"` GenericClientSecretFile string `mapstructure:"generic-client-secret-file"`
GenericUserURL string `mapstructure:"generic-user-url"` GenericScopes string `mapstructure:"generic-scopes"`
GenericName string `mapstructure:"generic-name"` GenericAuthURL string `mapstructure:"generic-auth-url"`
DisableContinue bool `mapstructure:"disable-continue"` GenericTokenURL string `mapstructure:"generic-token-url"`
OAuthWhitelist string `mapstructure:"oauth-whitelist"` GenericUserURL string `mapstructure:"generic-user-url"`
SessionExpiry int `mapstructure:"session-expiry"` GenericName string `mapstructure:"generic-name"`
LogLevel int8 `mapstructure:"log-level" validate:"min=-1,max=5"` DisableContinue bool `mapstructure:"disable-continue"`
Title string `mapstructure:"app-title"` OAuthWhitelist string `mapstructure:"oauth-whitelist"`
EnvFile string `mapstructure:"env-file"` SessionExpiry int `mapstructure:"session-expiry"`
LoginTimeout int `mapstructure:"login-timeout"` LogLevel int8 `mapstructure:"log-level" validate:"min=-1,max=5"`
LoginMaxRetries int `mapstructure:"login-max-retries"` Title string `mapstructure:"app-title"`
FogotPasswordMessage string `mapstructure:"forgot-password-message" validate:"required"` EnvFile string `mapstructure:"env-file"`
}
// Server configuration
type HandlersConfig struct {
AppURL string
Domain string
CookieSecure bool
DisableContinue bool
GenericName string
Title string
ForgotPasswordMessage string
}
// OAuthConfig is the configuration for the providers
type OAuthConfig struct {
GithubClientId string
GithubClientSecret string
GoogleClientId string
GoogleClientSecret string
GenericClientId string
GenericClientSecret string
GenericScopes []string
GenericAuthURL string
GenericTokenURL string
GenericUserURL string
AppURL string
} }
// APIConfig is the configuration for the API // APIConfig is the configuration for the API
@@ -67,19 +41,39 @@ type APIConfig struct {
Address string Address string
} }
// AuthConfig is the configuration for the auth service // OAuthConfig is the configuration for the providers
type AuthConfig struct { type OAuthConfig struct {
Users Users GithubClientId string
OauthWhitelist string GithubClientSecret string
SessionExpiry int GoogleClientId string
Secret string GoogleClientSecret string
CookieSecure bool TailscaleClientId string
Domain string TailscaleClientSecret string
LoginTimeout int GenericClientId string
LoginMaxRetries int GenericClientSecret string
GenericScopes []string
GenericAuthURL string
GenericTokenURL string
GenericUserURL string
AppURL string
} }
// HooksConfig is the configuration for the hooks service // Server configuration
type HooksConfig struct { type HandlersConfig struct {
Domain string AppURL string
Domain string
CookieSecure bool
DisableContinue bool
GenericName string
Title string
}
// Auth configuration
type AuthConfig struct {
Domain string
Secret string
CookieSecure bool
SessionExpiry int
Users Users
OAuthWhitelist []string
} }

View File

@@ -1,9 +1,6 @@
package types package types
import ( import "tinyauth/internal/oauth"
"time"
"tinyauth/internal/oauth"
)
// User is the struct for a user // User is the struct for a user
type User struct { type User struct {
@@ -25,37 +22,33 @@ type OAuthProviders struct {
// SessionCookie is the cookie for the session (exculding the expiry) // SessionCookie is the cookie for the session (exculding the expiry)
type SessionCookie struct { type SessionCookie struct {
Username string Username string
Name string
Email string
Provider string Provider string
TotpPending bool TotpPending bool
OAuthGroups string
} }
// TinyauthLabels is the labels for the tinyauth container // TinyauthLabels is the labels for the tinyauth container
type TinyauthLabels struct { type TinyauthLabels struct {
OAuthWhitelist string OAuthWhitelist []string
Users string Users []string
Allowed string Allowed string
Headers map[string]string Headers map[string]string
OAuthGroups string
} }
// UserContext is the context for the user // UserContext is the context for the user
type UserContext struct { type UserContext struct {
Username string Username string
Name string
Email string
IsLoggedIn bool IsLoggedIn bool
OAuth bool OAuth bool
Provider string Provider string
TotpPending bool TotpPending bool
OAuthGroups string
} }
// LoginAttempt tracks information about login attempts for rate limiting // App Context is the response for the app context endpoint
type LoginAttempt struct { type AppContext struct {
FailedAttempts int Status int `json:"status"`
LastAttempt time.Time Message string `json:"message"`
LockedUntil time.Time ConfiguredProviders []string `json:"configuredProviders"`
DisableContinue bool `json:"disableContinue"`
Title string `json:"title"`
GenericName string `json:"genericName"`
} }

View File

@@ -4,7 +4,6 @@ import (
"errors" "errors"
"net/url" "net/url"
"os" "os"
"regexp"
"slices" "slices"
"strings" "strings"
"tinyauth/internal/constants" "tinyauth/internal/constants"
@@ -131,7 +130,7 @@ func GetSecret(conf string, file string) string {
} }
// Return the contents of the file // Return the contents of the file
return ParseSecretFile(contents) return contents
} }
// Get the users from the config or file // Get the users from the config or file
@@ -189,9 +188,9 @@ func GetTinyauthLabels(labels map[string]string) types.TinyauthLabels {
// Add the label value to the tinyauth labels struct // Add the label value to the tinyauth labels struct
switch label { switch label {
case "tinyauth.oauth.whitelist": case "tinyauth.oauth.whitelist":
tinyauthLabels.OAuthWhitelist = value tinyauthLabels.OAuthWhitelist = strings.Split(value, ",")
case "tinyauth.users": case "tinyauth.users":
tinyauthLabels.Users = value tinyauthLabels.Users = strings.Split(value, ",")
case "tinyauth.allowed": case "tinyauth.allowed":
tinyauthLabels.Allowed = value tinyauthLabels.Allowed = value
case "tinyauth.headers": case "tinyauth.headers":
@@ -204,8 +203,6 @@ func GetTinyauthLabels(labels map[string]string) types.TinyauthLabels {
} }
tinyauthLabels.Headers[headerSplit[0]] = headerSplit[1] tinyauthLabels.Headers[headerSplit[0]] = headerSplit[1]
} }
case "tinyauth.oauth.groups":
tinyauthLabels.OAuthGroups = value
} }
} }
} }
@@ -216,7 +213,7 @@ func GetTinyauthLabels(labels map[string]string) types.TinyauthLabels {
// Check if any of the OAuth providers are configured based on the client id and secret // Check if any of the OAuth providers are configured based on the client id and secret
func OAuthConfigured(config types.Config) bool { func OAuthConfigured(config types.Config) bool {
return (config.GithubClientId != "" && config.GithubClientSecret != "") || (config.GoogleClientId != "" && config.GoogleClientSecret != "") || (config.GenericClientId != "" && config.GenericClientSecret != "") return (config.GithubClientId != "" && config.GithubClientSecret != "") || (config.GoogleClientId != "" && config.GoogleClientSecret != "") || (config.GenericClientId != "" && config.GenericClientSecret != "") || (config.TailscaleClientId != "" && config.TailscaleClientSecret != "")
} }
// Filter helper function // Filter helper function
@@ -244,21 +241,23 @@ func ParseUser(user string) (types.User, error) {
return types.User{}, errors.New("invalid user format") return types.User{}, errors.New("invalid user format")
} }
// Check for empty strings
for _, userPart := range userSplit {
if strings.TrimSpace(userPart) == "" {
return types.User{}, errors.New("invalid user format")
}
}
// Check if the user has a totp secret // Check if the user has a totp secret
if len(userSplit) == 2 { if len(userSplit) == 2 {
// Check for empty username or password
if userSplit[1] == "" || userSplit[0] == "" {
return types.User{}, errors.New("invalid user format")
}
return types.User{ return types.User{
Username: userSplit[0], Username: userSplit[0],
Password: userSplit[1], Password: userSplit[1],
}, nil }, nil
} }
// Check for empty username, password or totp secret
if userSplit[2] == "" || userSplit[1] == "" || userSplit[0] == "" {
return types.User{}, errors.New("invalid user format")
}
// Return the user struct // Return the user struct
return types.User{ return types.User{
Username: userSplit[0], Username: userSplit[0],
@@ -266,81 +265,3 @@ func ParseUser(user string) (types.User, error) {
TotpSecret: userSplit[2], TotpSecret: userSplit[2],
}, nil }, nil
} }
// Parse secret file
func ParseSecretFile(contents string) string {
// Split to lines
lines := strings.Split(contents, "\n")
// Loop through the lines
for _, line := range lines {
// Check if the line is empty
if strings.TrimSpace(line) == "" {
continue
}
// Return the line
return strings.TrimSpace(line)
}
// Return an empty string
return ""
}
// Check if a string matches a regex or a whitelist
func CheckWhitelist(whitelist string, str string) bool {
// Check if the whitelist is empty
if len(strings.TrimSpace(whitelist)) == 0 {
return true
}
// Check if the whitelist is a regex
if strings.HasPrefix(whitelist, "/") && strings.HasSuffix(whitelist, "/") {
// Create regex
re, err := regexp.Compile(whitelist[1 : len(whitelist)-1])
// Check if there was an error
if err != nil {
log.Error().Err(err).Msg("Error compiling regex")
return false
}
// Check if the string matches the regex
if re.MatchString(str) {
return true
}
}
// Split the whitelist by comma
whitelistSplit := strings.Split(whitelist, ",")
// Loop through the whitelist
for _, item := range whitelistSplit {
// Check if the item matches with the string
if strings.TrimSpace(item) == str {
return true
}
}
// 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)
}

View File

@@ -1,7 +1,6 @@
package utils_test package utils_test
import ( import (
"fmt"
"os" "os"
"reflect" "reflect"
"testing" "testing"
@@ -124,7 +123,7 @@ func TestGetSecret(t *testing.T) {
expected := "test" expected := "test"
// Create file // Create file
err := os.WriteFile(file, []byte(fmt.Sprintf("\n\n \n\n\n %s \n\n \n ", expected)), 0644) err := os.WriteFile(file, []byte(expected), 0644)
// Check if there was an error // Check if there was an error
if err != nil { if err != nil {
@@ -286,19 +285,15 @@ func TestGetTinyauthLabels(t *testing.T) {
// Test the get tinyauth labels function with a valid map // Test the get tinyauth labels function with a valid map
labels := map[string]string{ labels := map[string]string{
"tinyauth.users": "user1,user2", "tinyauth.users": "user1,user2",
"tinyauth.oauth.whitelist": "/regex/", "tinyauth.oauth.whitelist": "user1,user2",
"tinyauth.allowed": "random", "tinyauth.allowed": "random",
"random": "random", "random": "random",
"tinyauth.headers": "X-Header=value",
} }
expected := types.TinyauthLabels{ expected := types.TinyauthLabels{
Users: "user1,user2", Users: []string{"user1", "user2"},
OAuthWhitelist: "/regex/", OAuthWhitelist: []string{"user1", "user2"},
Allowed: "random", Allowed: "random",
Headers: map[string]string{
"X-Header": "value",
},
} }
result := utils.GetTinyauthLabels(labels) result := utils.GetTinyauthLabels(labels)
@@ -389,143 +384,3 @@ func TestParseUser(t *testing.T) {
t.Fatalf("Expected error parsing user") t.Fatalf("Expected error parsing user")
} }
} }
// Test the whitelist function
func TestCheckWhitelist(t *testing.T) {
t.Log("Testing check whitelist with a comma whitelist")
// Create variables
whitelist := "user1,user2,user3"
str := "user1"
expected := true
// Test the check whitelist function
result := utils.CheckWhitelist(whitelist, str)
// Check if the result is equal to the expected
if result != expected {
t.Fatalf("Expected %v, got %v", expected, result)
}
t.Log("Testing check whitelist with a regex whitelist")
// Create variables
whitelist = "/^user[0-9]+$/"
str = "user1"
expected = true
// Test the check whitelist function
result = utils.CheckWhitelist(whitelist, str)
// Check if the result is equal to the expected
if result != expected {
t.Fatalf("Expected %v, got %v", expected, result)
}
t.Log("Testing check whitelist with an empty whitelist")
// Create variables
whitelist = ""
str = "user1"
expected = true
// Test the check whitelist function
result = utils.CheckWhitelist(whitelist, str)
// Check if the result is equal to the expected
if result != expected {
t.Fatalf("Expected %v, got %v", expected, result)
}
t.Log("Testing check whitelist with an invalid regex whitelist")
// Create variables
whitelist = "/^user[0-9+$/"
str = "user1"
expected = false
// Test the check whitelist function
result = utils.CheckWhitelist(whitelist, str)
// Check if the result is equal to the expected
if result != expected {
t.Fatalf("Expected %v, got %v", expected, result)
}
t.Log("Testing check whitelist with a non matching whitelist")
// Create variables
whitelist = "user1,user2,user3"
str = "user4"
expected = false
// Test the check whitelist function
result = utils.CheckWhitelist(whitelist, str)
// Check if the result is equal to the expected
if result != expected {
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)
}
}