Compare commits

..

1 Commits

Author SHA1 Message Date
Stavros
d0be4a2654 feat: add version information to login screen 2025-07-12 00:08:24 +03:00
163 changed files with 6454 additions and 8777 deletions

View File

@@ -1,22 +1,33 @@
PORT=3000
ADDRESS=0.0.0.0
SECRET=app_secret
SECRET_FILE=app_secret_file
APP_URL=http://localhost:3000
USERS=your_user_password_hash
USERS_FILE=users_file
SECURE_COOKIE=false
COOKIE_SECURE=false
GITHUB_CLIENT_ID=github_client_id
GITHUB_CLIENT_SECRET=github_client_secret
GITHUB_CLIENT_SECRET_FILE=github_client_secret_file
GOOGLE_CLIENT_ID=google_client_id
GOOGLE_CLIENT_SECRET=google_client_secret
GOOGLE_CLIENT_SECRET_FILE=google_client_secret_file
GENERIC_CLIENT_ID=generic_client_id
GENERIC_CLIENT_SECRET=generic_client_secret
GENERIC_CLIENT_SECRET_FILE=generic_client_secret_file
GENERIC_SCOPES=generic_scopes
GENERIC_AUTH_URL=generic_auth_url
GENERIC_TOKEN_URL=generic_token_url
GENERIC_USER_URL=generic_user_url
DISABLE_CONTINUE=false
OAUTH_WHITELIST=
GENERIC_NAME=My OAuth
SESSION_EXPIRY=7200
LOGIN_TIMEOUT=300
LOGIN_MAX_RETRIES=5
LOG_LEVEL=debug
LOG_LEVEL=0
APP_TITLE=Tinyauth SSO
FORGOT_PASSWORD_MESSAGE=Some message about resetting the password
OAUTH_AUTO_REDIRECT=none
BACKGROUND_IMAGE=some_image_url
GENERIC_SKIP_SSL=false
RESOURCES_DIR=/data/resources
DATABASE_PATH=/data/tinyauth.db
DISABLE_ANALYTICS=false
DISABLE_RESOURCES=false
TRUSTED_PROXIES=
GENERIC_SKIP_SSL=false

View File

@@ -23,17 +23,12 @@ jobs:
- name: Install frontend dependencies
run: |
cd frontend
bun install --frozen-lockfile
bun install
- name: Set version
run: |
echo testing > internal/assets/version
- name: Lint frontend
run: |
cd frontend
bun run lint
- name: Build frontend
run: |
cd frontend

View File

@@ -66,7 +66,7 @@ jobs:
- name: Install frontend dependencies
run: |
cd frontend
bun install --frozen-lockfile
bun install
- name: Install backend dependencies
run: |
@@ -80,7 +80,7 @@ jobs:
- name: Build
run: |
cp -r frontend/dist internal/assets/dist
go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64
go build -ldflags "-s -w -X tinyauth/internal/constants.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/constants.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/constants.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64
env:
CGO_ENABLED: 0
@@ -112,7 +112,7 @@ jobs:
- name: Install frontend dependencies
run: |
cd frontend
bun install --frozen-lockfile
bun install
- name: Install backend dependencies
run: |
@@ -126,7 +126,7 @@ jobs:
- name: Build
run: |
cp -r frontend/dist internal/assets/dist
go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64
go build -ldflags "-s -w -X tinyauth/internal/constants.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/constants.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/constants.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64
env:
CGO_ENABLED: 0
@@ -171,9 +171,6 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha
cache-to: type=gha,mode=max
github-token: ${{ secrets.GITHUB_TOKEN }}
build-args: |
VERSION=${{ needs.generate-metadata.outputs.VERSION }}
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
@@ -193,65 +190,6 @@ jobs:
if-no-files-found: error
retention-days: 1
image-build-distroless:
runs-on: ubuntu-latest
needs:
- create-release
- generate-metadata
- image-build
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: nightly
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
id: build
with:
platforms: linux/amd64
labels: ${{ steps.meta.outputs.labels }}
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
file: Dockerfile.distroless
cache-from: type=gha
cache-to: type=gha,mode=max
github-token: ${{ secrets.GITHUB_TOKEN }}
build-args: |
VERSION=${{ needs.generate-metadata.outputs.VERSION }}
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}
- name: Export digest
run: |
mkdir -p ${{ runner.temp }}/digests
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-distroless-linux-amd64
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
image-build-arm:
runs-on: ubuntu-24.04-arm
needs:
@@ -279,6 +217,10 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set version
run: |
echo nightly > internal/assets/version
- name: Build and push
uses: docker/build-push-action@v6
id: build
@@ -287,9 +229,6 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha
cache-to: type=gha,mode=max
github-token: ${{ secrets.GITHUB_TOKEN }}
build-args: |
VERSION=${{ needs.generate-metadata.outputs.VERSION }}
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
@@ -309,65 +248,6 @@ jobs:
if-no-files-found: error
retention-days: 1
image-build-arm-distroless:
runs-on: ubuntu-24.04-arm
needs:
- create-release
- generate-metadata
- image-build-arm
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: nightly
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
id: build
with:
platforms: linux/arm64
labels: ${{ steps.meta.outputs.labels }}
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
file: Dockerfile.distroless
cache-from: type=gha
cache-to: type=gha,mode=max
github-token: ${{ secrets.GITHUB_TOKEN }}
build-args: |
VERSION=${{ needs.generate-metadata.outputs.VERSION }}
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}
- name: Export digest
run: |
mkdir -p ${{ runner.temp }}/digests
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-distroless-linux-arm64
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
image-merge:
runs-on: ubuntu-latest
needs:
@@ -396,8 +276,6 @@ jobs:
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth
flavor: |
latest=false
tags: |
type=raw,nightly
@@ -407,45 +285,6 @@ jobs:
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *)
image-merge-distroless:
runs-on: ubuntu-latest
needs:
- image-build-distroless
- image-build-arm-distroless
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: ${{ runner.temp }}/digests
pattern: digests-distroless-*
merge-multiple: true
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth
flavor: |
latest=false
tags: |
type=raw,nightly-distroless
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *)
update-release:
runs-on: ubuntu-latest
needs:

View File

@@ -44,7 +44,7 @@ jobs:
- name: Install frontend dependencies
run: |
cd frontend
bun install --frozen-lockfile
bun install
- name: Install backend dependencies
run: |
@@ -58,7 +58,7 @@ jobs:
- name: Build
run: |
cp -r frontend/dist internal/assets/dist
go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64
go build -ldflags "-s -w -X tinyauth/internal/constants.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/constants.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/constants.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64
env:
CGO_ENABLED: 0
@@ -87,7 +87,7 @@ jobs:
- name: Install frontend dependencies
run: |
cd frontend
bun install --frozen-lockfile
bun install
- name: Install backend dependencies
run: |
@@ -101,7 +101,7 @@ jobs:
- name: Build
run: |
cp -r frontend/dist internal/assets/dist
go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64
go build -ldflags "-s -w -X tinyauth/internal/constants.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/constants.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/constants.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64
env:
CGO_ENABLED: 0
@@ -143,9 +143,6 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha
cache-to: type=gha,mode=max
github-token: ${{ secrets.GITHUB_TOKEN }}
build-args: |
VERSION=${{ needs.generate-metadata.outputs.VERSION }}
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
@@ -165,62 +162,6 @@ jobs:
if-no-files-found: error
retention-days: 1
image-build-distroless:
runs-on: ubuntu-latest
needs:
- generate-metadata
- image-build
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
id: build
with:
platforms: linux/amd64
labels: ${{ steps.meta.outputs.labels }}
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
file: Dockerfile.distroless
cache-from: type=gha
cache-to: type=gha,mode=max
github-token: ${{ secrets.GITHUB_TOKEN }}
build-args: |
VERSION=${{ needs.generate-metadata.outputs.VERSION }}
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}
- name: Export digest
run: |
mkdir -p ${{ runner.temp }}/digests
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-distroless-linux-amd64
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
image-build-arm:
runs-on: ubuntu-24.04-arm
needs:
@@ -253,9 +194,6 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha
cache-to: type=gha,mode=max
github-token: ${{ secrets.GITHUB_TOKEN }}
build-args: |
VERSION=${{ needs.generate-metadata.outputs.VERSION }}
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
@@ -275,62 +213,6 @@ jobs:
if-no-files-found: error
retention-days: 1
image-build-arm-distroless:
runs-on: ubuntu-24.04-arm
needs:
- generate-metadata
- image-build-arm
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
id: build
with:
platforms: linux/arm64
labels: ${{ steps.meta.outputs.labels }}
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
file: Dockerfile.distroless
cache-from: type=gha
cache-to: type=gha,mode=max
github-token: ${{ secrets.GITHUB_TOKEN }}
build-args: |
VERSION=${{ needs.generate-metadata.outputs.VERSION }}
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}
- name: Export digest
run: |
mkdir -p ${{ runner.temp }}/digests
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-distroless-linux-arm64
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
image-merge:
runs-on: ubuntu-latest
needs:
@@ -359,55 +241,10 @@ jobs:
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth
flavor: |
prefix=v,onlatest=false
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *)
image-merge-distroless:
runs-on: ubuntu-latest
needs:
- image-build-distroless
- image-build-arm-distroless
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: ${{ runner.temp }}/digests
pattern: digests-distroless-*
merge-multiple: true
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth
flavor: |
latest=false
prefix=v
suffix=-distroless
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{version}},prefix=v
type=semver,pattern={{major}},prefix=v
type=semver,pattern={{major}}.{{minor}},prefix=v
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests

5
.gitignore vendored
View File

@@ -23,7 +23,4 @@ secret*
tmp
# version files
internal/assets/version
# data directory
data
internal/assets/version

View File

@@ -1,12 +1,12 @@
# Site builder
FROM oven/bun:1.3.4-alpine AS frontend-builder
FROM oven/bun:1.2.18-alpine AS frontend-builder
WORKDIR /frontend
COPY ./frontend/package.json ./
COPY ./frontend/bun.lock ./
RUN bun install --frozen-lockfile
RUN bun install
COPY ./frontend/public ./public
COPY ./frontend/src ./src
@@ -20,7 +20,7 @@ COPY ./frontend/vite.config.ts ./
RUN bun run build
# Builder
FROM golang:1.25-alpine3.21 AS builder
FROM golang:1.24-alpine3.21 AS builder
ARG VERSION
ARG COMMIT_HASH
@@ -38,25 +38,17 @@ COPY ./cmd ./cmd
COPY ./internal ./internal
COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
RUN CGO_ENABLED=0 go build -ldflags "-s -w -X tinyauth/internal/config.Version=${VERSION} -X tinyauth/internal/config.CommitHash=${COMMIT_HASH} -X tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}"
RUN CGO_ENABLED=0 go build -ldflags "-s -w -X tinyauth/internal/constants.Version=${VERSION} -X tinyauth/internal/constants.CommitHash=${COMMIT_HASH} -X tinyauth/internal/constants.BuildTimestamp=${BUILD_TIMESTAMP}"
# Runner
FROM alpine:3.23 AS runner
FROM alpine:3.22 AS runner
WORKDIR /tinyauth
COPY --from=builder /tinyauth/tinyauth ./
RUN apk add --no-cache curl
RUN mkdir -p /data
COPY --from=builder /tinyauth/tinyauth ./
EXPOSE 3000
VOLUME ["/data"]
ENV GIN_MODE=release
ENV PATH=$PATH:/tinyauth
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 CMD ["tinyauth", "healthcheck"]
ENTRYPOINT ["tinyauth"]
ENTRYPOINT ["./tinyauth"]

View File

@@ -1,4 +1,4 @@
FROM golang:1.25-alpine3.21
FROM golang:1.24-alpine3.21
WORKDIR /tinyauth
@@ -7,14 +7,13 @@ COPY go.sum ./
RUN go mod download
RUN go install github.com/air-verse/air@v1.61.7
RUN go install github.com/go-delve/delve/cmd/dlv@latest
COPY ./cmd ./cmd
COPY ./internal ./internal
COPY ./main.go ./
COPY ./air.toml ./
RUN go install github.com/air-verse/air@v1.61.7
EXPOSE 3000
ENTRYPOINT ["air", "-c", "air.toml"]

View File

@@ -1,65 +0,0 @@
# Site builder
FROM oven/bun:1.3.4-alpine AS frontend-builder
WORKDIR /frontend
COPY ./frontend/package.json ./
COPY ./frontend/bun.lock ./
RUN bun install --frozen-lockfile
COPY ./frontend/public ./public
COPY ./frontend/src ./src
COPY ./frontend/eslint.config.js ./
COPY ./frontend/index.html ./
COPY ./frontend/tsconfig.json ./
COPY ./frontend/tsconfig.app.json ./
COPY ./frontend/tsconfig.node.json ./
COPY ./frontend/vite.config.ts ./
RUN bun run build
# Builder
FROM golang:1.25-alpine3.21 AS builder
ARG VERSION
ARG COMMIT_HASH
ARG BUILD_TIMESTAMP
WORKDIR /tinyauth
COPY go.mod ./
COPY go.sum ./
RUN go mod download
COPY ./main.go ./
COPY ./cmd ./cmd
COPY ./internal ./internal
COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
RUN mkdir -p data
RUN CGO_ENABLED=0 go build -ldflags "-s -w -X tinyauth/internal/config.Version=${VERSION} -X tinyauth/internal/config.CommitHash=${COMMIT_HASH} -X tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}"
# Runner
FROM gcr.io/distroless/static-debian12:latest AS runner
WORKDIR /tinyauth
COPY --from=builder /tinyauth/tinyauth ./
# Since it's distroless, we need to copy the data directory from the builder stage
COPY --from=builder /tinyauth/data /data
EXPOSE 3000
VOLUME ["/data"]
ENV GIN_MODE=release
ENV PATH=$PATH:/tinyauth
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 CMD ["tinyauth", "healthcheck"]
ENTRYPOINT ["tinyauth"]

View File

@@ -1,7 +1,7 @@
<div align="center">
<img alt="Tinyauth" title="Tinyauth" width="96" src="assets/logo-rounded.png">
<h1>Tinyauth</h1>
<p>The simplest way to protect your apps with a login screen.</p>
<p>The easiest way to secure your apps with a login screen.</p>
</div>
<div align="center">
@@ -14,7 +14,7 @@
<br />
Tinyauth is a simple authentication middleware that adds a simple login screen or OAuth with Google, Github or any other provider to all of your apps. It supports all the popular proxies like Traefik, Nginx and Caddy.
Tinyauth is a simple authentication middleware that adds a simple login screen or OAuth with Google, Github and any provider to all of your docker apps. It supports all the popular proxies like Traefik, Nginx and Caddy.
![Screenshot](assets/screenshot.png)
@@ -23,7 +23,7 @@ Tinyauth is a simple authentication middleware that adds a simple login screen o
## Getting Started
You can easily get started with Tinyauth by following the guide in the [documentation](https://tinyauth.app/docs/getting-started). There is also an available [docker compose](./docker-compose.example.yml) file that has Traefik, Whoami and Tinyauth to demonstrate its capabilities.
You can easily get started with Tinyauth by following the guide in the [documentation](https://tinyauth.app/docs/getting-started.html). There is also an available [docker compose](./docker-compose.example.yml) file that has Traefik, Whoami and Tinyauth to demonstrate its capabilities.
## Demo
@@ -53,7 +53,7 @@ Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You ma
A big thank you to the following people for providing me with more coffee:
<!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https:&#x2F;&#x2F;github.com&#x2F;erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a>&nbsp;&nbsp;<a href="https://github.com/nicotsx"><img src="https:&#x2F;&#x2F;github.com&#x2F;nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a>&nbsp;&nbsp;<a href="https://github.com/SimpleHomelab"><img src="https:&#x2F;&#x2F;github.com&#x2F;SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a>&nbsp;&nbsp;<a href="https://github.com/jmadden91"><img src="https:&#x2F;&#x2F;github.com&#x2F;jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a>&nbsp;&nbsp;<a href="https://github.com/tribor"><img src="https:&#x2F;&#x2F;github.com&#x2F;tribor.png" width="64px" alt="User avatar: tribor" /></a>&nbsp;&nbsp;<a href="https://github.com/eliasbenb"><img src="https:&#x2F;&#x2F;github.com&#x2F;eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a>&nbsp;&nbsp;<a href="https://github.com/afunworm"><img src="https:&#x2F;&#x2F;github.com&#x2F;afunworm.png" width="64px" alt="User avatar: afunworm" /></a>&nbsp;&nbsp;<a href="https://github.com/chip-well"><img src="https:&#x2F;&#x2F;github.com&#x2F;chip-well.png" width="64px" alt="User avatar: chip-well" /></a>&nbsp;&nbsp;<a href="https://github.com/Lancelot-Enguerrand"><img src="https:&#x2F;&#x2F;github.com&#x2F;Lancelot-Enguerrand.png" width="64px" alt="User avatar: Lancelot-Enguerrand" /></a>&nbsp;&nbsp;<a href="https://github.com/allgoewer"><img src="https:&#x2F;&#x2F;github.com&#x2F;allgoewer.png" width="64px" alt="User avatar: allgoewer" /></a>&nbsp;&nbsp;<!-- sponsors -->
<!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https:&#x2F;&#x2F;github.com&#x2F;erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a>&nbsp;&nbsp;<a href="https://github.com/nicotsx"><img src="https:&#x2F;&#x2F;github.com&#x2F;nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a>&nbsp;&nbsp;<a href="https://github.com/SimpleHomelab"><img src="https:&#x2F;&#x2F;github.com&#x2F;SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a>&nbsp;&nbsp;<a href="https://github.com/jmadden91"><img src="https:&#x2F;&#x2F;github.com&#x2F;jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a>&nbsp;&nbsp;<a href="https://github.com/tribor"><img src="https:&#x2F;&#x2F;github.com&#x2F;tribor.png" width="64px" alt="User avatar: tribor" /></a>&nbsp;&nbsp;<a href="https://github.com/eliasbenb"><img src="https:&#x2F;&#x2F;github.com&#x2F;eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a>&nbsp;&nbsp;<a href="https://github.com/afunworm"><img src="https:&#x2F;&#x2F;github.com&#x2F;afunworm.png" width="64px" alt="User avatar: afunworm" /></a>&nbsp;&nbsp;<!-- sponsors -->
## Acknowledgements

View File

@@ -2,10 +2,9 @@ root = "/tinyauth"
tmp_dir = "tmp"
[build]
pre_cmd = ["mkdir -p internal/assets/dist", "mkdir -p /data", "echo 'backend running' > internal/assets/dist/index.html"]
pre_cmd = ["mkdir -p internal/assets/dist", "echo 'backend running' > internal/assets/dist/index.html", "go install github.com/go-delve/delve/cmd/dlv@v1.25.0"]
cmd = "CGO_ENABLED=0 go build -gcflags=\"all=-N -l\" -o tmp/tinyauth ."
bin = "tmp/tinyauth"
full_bin = "dlv --listen :4000 --headless=true --api-version=2 --accept-multiclient --log=true exec tmp/tinyauth --continue --check-go-version=false"
bin = "/go/bin/dlv --listen :4000 --headless=true --api-version=2 --accept-multiclient --log=true exec tmp/tinyauth --continue"
include_ext = ["go"]
exclude_dir = ["internal/assets/dist"]
exclude_regex = [".*_test\\.go"]

View File

@@ -1,99 +0,0 @@
package cmd
import (
"errors"
"fmt"
"strings"
"github.com/charmbracelet/huh"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"golang.org/x/crypto/bcrypt"
)
type createUserCmd struct {
root *cobra.Command
cmd *cobra.Command
interactive bool
docker bool
username string
password string
}
func newCreateUserCmd(root *cobra.Command) *createUserCmd {
return &createUserCmd{
root: root,
}
}
func (c *createUserCmd) Register() {
c.cmd = &cobra.Command{
Use: "create",
Short: "Create a user",
Long: `Create a user either interactively or by passing flags.`,
Run: c.run,
}
c.cmd.Flags().BoolVarP(&c.interactive, "interactive", "i", false, "Create a user interactively")
c.cmd.Flags().BoolVar(&c.docker, "docker", false, "Format output for docker")
c.cmd.Flags().StringVar(&c.username, "username", "", "Username")
c.cmd.Flags().StringVar(&c.password, "password", "", "Password")
if c.root != nil {
c.root.AddCommand(c.cmd)
}
}
func (c *createUserCmd) GetCmd() *cobra.Command {
return c.cmd
}
func (c *createUserCmd) run(cmd *cobra.Command, args []string) {
log.Logger = log.Level(zerolog.InfoLevel)
if c.interactive {
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().Title("Username").Value(&c.username).Validate((func(s string) error {
if s == "" {
return errors.New("username cannot be empty")
}
return nil
})),
huh.NewInput().Title("Password").Value(&c.password).Validate((func(s string) error {
if s == "" {
return errors.New("password cannot be empty")
}
return nil
})),
huh.NewSelect[bool]().Title("Format the output for Docker?").Options(huh.NewOption("Yes", true), huh.NewOption("No", false)).Value(&c.docker),
),
)
var baseTheme *huh.Theme = huh.ThemeBase()
err := form.WithTheme(baseTheme).Run()
if err != nil {
log.Fatal().Err(err).Msg("Form failed")
}
}
if c.username == "" || c.password == "" {
log.Fatal().Err(errors.New("error invalid input")).Msg("Username and password cannot be empty")
}
log.Info().Str("username", c.username).Msg("Creating user")
passwd, err := bcrypt.GenerateFromPassword([]byte(c.password), bcrypt.DefaultCost)
if err != nil {
log.Fatal().Err(err).Msg("Failed to hash password")
}
// If docker format is enabled, escape the dollar sign
passwdStr := string(passwd)
if c.docker {
passwdStr = strings.ReplaceAll(passwdStr, "$", "$$")
}
log.Info().Str("user", fmt.Sprintf("%s:%s", c.username, passwdStr)).Msg("User created")
}

View File

@@ -1,120 +0,0 @@
package cmd
import (
"errors"
"fmt"
"os"
"strings"
"tinyauth/internal/utils"
"github.com/charmbracelet/huh"
"github.com/mdp/qrterminal/v3"
"github.com/pquerna/otp/totp"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
type generateTotpCmd struct {
root *cobra.Command
cmd *cobra.Command
interactive bool
user string
}
func newGenerateTotpCmd(root *cobra.Command) *generateTotpCmd {
return &generateTotpCmd{
root: root,
}
}
func (c *generateTotpCmd) Register() {
c.cmd = &cobra.Command{
Use: "generate",
Short: "Generate a totp secret",
Long: `Generate a totp secret for a user either interactively or by passing flags.`,
Run: c.run,
}
c.cmd.Flags().BoolVarP(&c.interactive, "interactive", "i", false, "Run in interactive mode")
c.cmd.Flags().StringVar(&c.user, "user", "", "Your current user (username:hash)")
if c.root != nil {
c.root.AddCommand(c.cmd)
}
}
func (c *generateTotpCmd) GetCmd() *cobra.Command {
return c.cmd
}
func (c *generateTotpCmd) run(cmd *cobra.Command, args []string) {
log.Logger = log.Level(zerolog.InfoLevel)
if c.interactive {
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().Title("Current user (username:hash)").Value(&c.user).Validate((func(s string) error {
if s == "" {
return errors.New("user cannot be empty")
}
return nil
})),
),
)
var baseTheme *huh.Theme = huh.ThemeBase()
err := form.WithTheme(baseTheme).Run()
if err != nil {
log.Fatal().Err(err).Msg("Form failed")
}
}
user, err := utils.ParseUser(c.user)
if err != nil {
log.Fatal().Err(err).Msg("Failed to parse user")
}
docker := false
if strings.Contains(c.user, "$$") {
docker = true
}
if user.TotpSecret != "" {
log.Fatal().Msg("User already has a TOTP secret")
}
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "Tinyauth",
AccountName: user.Username,
})
if err != nil {
log.Fatal().Err(err).Msg("Failed to generate TOTP secret")
}
secret := key.Secret()
log.Info().Str("secret", secret).Msg("Generated TOTP secret")
log.Info().Msg("Generated QR code")
config := qrterminal.Config{
Level: qrterminal.L,
Writer: os.Stdout,
BlackChar: qrterminal.BLACK,
WhiteChar: qrterminal.WHITE,
QuietZone: 2,
}
qrterminal.GenerateWithConfig(key.URL(), config)
user.TotpSecret = secret
// If using docker escape re-escape it
if docker {
user.Password = strings.ReplaceAll(user.Password, "$", "$$")
}
log.Info().Str("user", fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TotpSecret)).Msg("Add the totp secret to your authenticator app then use the verify command to ensure everything is working correctly.")
}

View File

@@ -1,112 +0,0 @@
package cmd
import (
"encoding/json"
"errors"
"io"
"net/http"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
type healthzResponse struct {
Status string `json:"status"`
Message string `json:"message"`
}
type healthcheckCmd struct {
root *cobra.Command
cmd *cobra.Command
viper *viper.Viper
}
func newHealthcheckCmd(root *cobra.Command) *healthcheckCmd {
return &healthcheckCmd{
root: root,
viper: viper.New(),
}
}
func (c *healthcheckCmd) Register() {
c.cmd = &cobra.Command{
Use: "healthcheck [app-url]",
Short: "Perform a health check",
Long: `Use the health check endpoint to verify that Tinyauth is running and it's healthy.`,
Run: c.run,
}
c.viper.AutomaticEnv()
if c.root != nil {
c.root.AddCommand(c.cmd)
}
}
func (c *healthcheckCmd) GetCmd() *cobra.Command {
return c.cmd
}
func (c *healthcheckCmd) run(cmd *cobra.Command, args []string) {
log.Logger = log.Level(zerolog.InfoLevel)
var appUrl string
port := c.viper.GetString("PORT")
address := c.viper.GetString("ADDRESS")
if port == "" {
port = "3000"
}
if address == "" {
address = "127.0.0.1"
}
appUrl = "http://" + address + ":" + port
if len(args) > 0 {
appUrl = args[0]
}
log.Info().Str("app_url", appUrl).Msg("Performing health check")
client := http.Client{}
req, err := http.NewRequest("GET", appUrl+"/api/healthz", nil)
if err != nil {
log.Fatal().Err(err).Msg("Failed to create request")
}
resp, err := client.Do(req)
if err != nil {
log.Fatal().Err(err).Msg("Failed to perform request")
}
if resp.StatusCode != http.StatusOK {
log.Fatal().Err(errors.New("service is not healthy")).Msgf("Service is not healthy. Status code: %d", resp.StatusCode)
}
defer resp.Body.Close()
var healthResp healthzResponse
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal().Err(err).Msg("Failed to read response")
}
err = json.Unmarshal(body, &healthResp)
if err != nil {
log.Fatal().Err(err).Msg("Failed to decode response")
}
log.Info().Interface("response", healthResp).Msg("Tinyauth is healthy")
}

View File

@@ -1,9 +1,22 @@
package cmd
import (
"errors"
"fmt"
"os"
"strings"
"tinyauth/internal/bootstrap"
"tinyauth/internal/config"
"time"
totpCmd "tinyauth/cmd/totp"
userCmd "tinyauth/cmd/user"
"tinyauth/internal/auth"
"tinyauth/internal/constants"
"tinyauth/internal/docker"
"tinyauth/internal/handlers"
"tinyauth/internal/hooks"
"tinyauth/internal/ldap"
"tinyauth/internal/providers"
"tinyauth/internal/server"
"tinyauth/internal/types"
"tinyauth/internal/utils"
"github.com/go-playground/validator/v10"
@@ -13,150 +26,273 @@ import (
"github.com/spf13/viper"
)
type rootCmd struct {
root *cobra.Command
cmd *cobra.Command
var rootCmd = &cobra.Command{
Use: "tinyauth",
Short: "The simplest way to protect your apps with a login screen.",
Long: `Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic OAuth provider to all of your docker apps.`,
Run: func(cmd *cobra.Command, args []string) {
// Logger
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Logger().Level(zerolog.FatalLevel)
viper *viper.Viper
}
// Get config
var config types.Config
err := viper.Unmarshal(&config)
HandleError(err, "Failed to parse config")
func newRootCmd() *rootCmd {
return &rootCmd{
viper: viper.New(),
}
}
// Secrets
config.Secret = utils.GetSecret(config.Secret, config.SecretFile)
config.GithubClientSecret = utils.GetSecret(config.GithubClientSecret, config.GithubClientSecretFile)
config.GoogleClientSecret = utils.GetSecret(config.GoogleClientSecret, config.GoogleClientSecretFile)
config.GenericClientSecret = utils.GetSecret(config.GenericClientSecret, config.GenericClientSecretFile)
func (c *rootCmd) Register() {
c.cmd = &cobra.Command{
Use: "tinyauth",
Short: "The simplest way to protect your apps with a login screen",
Long: `Tinyauth is a simple authentication middleware that adds a simple login screen or OAuth with Google, Github or any other provider to all of your docker apps.`,
Run: c.run,
}
// Validate config
validator := validator.New()
err = validator.Struct(config)
HandleError(err, "Failed to validate config")
// Ignore unknown flags to allow --providers-*
c.cmd.FParseErrWhitelist.UnknownFlags = true
// Logger
log.Logger = log.Level(zerolog.Level(config.LogLevel))
log.Info().Str("version", strings.TrimSpace(constants.Version)).Msg("Starting tinyauth")
c.viper.AutomaticEnv()
// Users
log.Info().Msg("Parsing users")
users, err := utils.GetUsers(config.Users, config.UsersFile)
HandleError(err, "Failed to parse users")
configOptions := []struct {
name string
defaultVal any
description string
}{
{"port", 3000, "Port to run the server on."},
{"address", "0.0.0.0", "Address to bind the server to."},
{"app-url", "", "The Tinyauth URL."},
{"users", "", "Comma separated list of users in the format username:hash."},
{"users-file", "", "Path to a file containing users in the format username:hash."},
{"secure-cookie", false, "Send cookie over secure connection only."},
{"oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth."},
{"oauth-auto-redirect", "none", "Auto redirect to the specified OAuth provider if configured. (available providers: github, google, generic)"},
{"session-expiry", 86400, "Session (cookie) expiration time in seconds."},
{"login-timeout", 300, "Login timeout in seconds after max retries reached (0 to disable)."},
{"login-max-retries", 5, "Maximum login attempts before timeout (0 to disable)."},
{"log-level", "info", "Log level."},
{"app-title", "Tinyauth", "Title of the app."},
{"forgot-password-message", "", "Message to show on the forgot password page."},
{"background-image", "/background.jpg", "Background image URL for the login page."},
{"ldap-address", "", "LDAP server address (e.g. ldap://localhost:389)."},
{"ldap-bind-dn", "", "LDAP bind DN (e.g. uid=user,dc=example,dc=com)."},
{"ldap-bind-password", "", "LDAP bind password."},
{"ldap-base-dn", "", "LDAP base DN (e.g. dc=example,dc=com)."},
{"ldap-insecure", false, "Skip certificate verification for the LDAP server."},
{"ldap-search-filter", "(uid=%s)", "LDAP search filter for user lookup."},
{"resources-dir", "/data/resources", "Path to a directory containing custom resources (e.g. background image)."},
{"database-path", "/data/tinyauth.db", "Path to the Sqlite database file. Directory will be created if it doesn't exist."},
{"trusted-proxies", "", "Comma separated list of trusted proxies (IP addresses or CIDRs) for correct client IP detection."},
{"disable-analytics", false, "Disable anonymous version collection."},
{"disable-resources", false, "Disable the resources server."},
{"socket-path", "", "Path to the Unix socket to bind the server to."},
{"disable-ui-warnings", false, "Disable UI warnings about insecure configurations."},
}
// Get domain
log.Debug().Msg("Getting domain")
domain, err := utils.GetUpperDomain(config.AppURL)
HandleError(err, "Failed to get upper domain")
log.Info().Str("domain", domain).Msg("Using domain for cookie store")
for _, opt := range configOptions {
switch v := opt.defaultVal.(type) {
case bool:
c.cmd.Flags().Bool(opt.name, v, opt.description)
case int:
c.cmd.Flags().Int(opt.name, v, opt.description)
case string:
c.cmd.Flags().String(opt.name, v, opt.description)
// Generate cookie name
cookieId := utils.GenerateIdentifier(strings.Split(domain, ".")[0])
sessionCookieName := fmt.Sprintf("%s-%s", constants.SessionCookieName, cookieId)
csrfCookieName := fmt.Sprintf("%s-%s", constants.CsrfCookieName, cookieId)
redirectCookieName := fmt.Sprintf("%s-%s", constants.RedirectCookieName, cookieId)
// Generate HMAC and encryption secrets
log.Debug().Msg("Deriving HMAC and encryption secrets")
hmacSecret, err := utils.DeriveKey(config.Secret, "hmac")
HandleError(err, "Failed to derive HMAC secret")
encryptionSecret, err := utils.DeriveKey(config.Secret, "encryption")
HandleError(err, "Failed to derive encryption secret")
// Create OAuth config
oauthConfig := types.OAuthConfig{
GithubClientId: config.GithubClientId,
GithubClientSecret: config.GithubClientSecret,
GoogleClientId: config.GoogleClientId,
GoogleClientSecret: config.GoogleClientSecret,
GenericClientId: config.GenericClientId,
GenericClientSecret: config.GenericClientSecret,
GenericScopes: strings.Split(config.GenericScopes, ","),
GenericAuthURL: config.GenericAuthURL,
GenericTokenURL: config.GenericTokenURL,
GenericUserURL: config.GenericUserURL,
GenericSkipSSL: config.GenericSkipSSL,
AppURL: config.AppURL,
}
// Create uppercase env var name
envVar := strings.ReplaceAll(strings.ToUpper(opt.name), "-", "_")
c.viper.BindEnv(opt.name, envVar)
}
// Create handlers config
handlersConfig := types.HandlersConfig{
AppURL: config.AppURL,
DisableContinue: config.DisableContinue,
Title: config.Title,
GenericName: config.GenericName,
CookieSecure: config.CookieSecure,
Domain: domain,
ForgotPasswordMessage: config.FogotPasswordMessage,
BackgroundImage: config.BackgroundImage,
OAuthAutoRedirect: config.OAuthAutoRedirect,
CsrfCookieName: csrfCookieName,
RedirectCookieName: redirectCookieName,
}
c.viper.BindPFlags(c.cmd.Flags())
// Create server config
serverConfig := types.ServerConfig{
Port: config.Port,
Address: config.Address,
}
if c.root != nil {
c.root.AddCommand(c.cmd)
// Create auth config
authConfig := types.AuthConfig{
Users: users,
OauthWhitelist: config.OAuthWhitelist,
CookieSecure: config.CookieSecure,
SessionExpiry: config.SessionExpiry,
Domain: domain,
LoginTimeout: config.LoginTimeout,
LoginMaxRetries: config.LoginMaxRetries,
SessionCookieName: sessionCookieName,
HMACSecret: hmacSecret,
EncryptionSecret: encryptionSecret,
}
// Create hooks config
hooksConfig := types.HooksConfig{
Domain: domain,
}
// Create docker service
docker, err := docker.NewDocker()
HandleError(err, "Failed to initialize docker")
// Create LDAP service if configured
var ldapService *ldap.LDAP
if config.LdapAddress != "" {
log.Info().Msg("Using LDAP for authentication")
ldapConfig := types.LdapConfig{
Address: config.LdapAddress,
BindDN: config.LdapBindDN,
BindPassword: config.LdapBindPassword,
BaseDN: config.LdapBaseDN,
Insecure: config.LdapInsecure,
SearchFilter: config.LdapSearchFilter,
}
// Create LDAP service
ldapService, err = ldap.NewLDAP(ldapConfig)
HandleError(err, "Failed to create LDAP service")
} else {
log.Info().Msg("LDAP not configured, using local users or OAuth")
}
// Check if we have any users configured
if len(users) == 0 && !utils.OAuthConfigured(config) && ldapService == nil {
HandleError(errors.New("err no users"), "Unable to find a source of users")
}
// Create auth service
auth := auth.NewAuth(authConfig, docker, ldapService)
// Create OAuth providers service
providers := providers.NewProviders(oauthConfig)
// Create hooks service
hooks := hooks.NewHooks(hooksConfig, auth, providers)
// Create handlers
handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker)
// Create server
srv, err := server.NewServer(serverConfig, handlers)
HandleError(err, "Failed to create server")
// Start server
err = srv.Start()
HandleError(err, "Failed to start server")
},
}
func Execute() {
err := rootCmd.Execute()
HandleError(err, "Failed to execute root command")
}
func HandleError(err error, msg string) {
// If error, log it and exit
if err != nil {
log.Fatal().Err(err).Msg(msg)
}
}
func (c *rootCmd) GetCmd() *cobra.Command {
return c.cmd
}
func (c *rootCmd) run(cmd *cobra.Command, args []string) {
var conf config.Config
err := c.viper.Unmarshal(&conf)
if err != nil {
log.Fatal().Err(err).Msg("Failed to parse config")
}
v := validator.New()
err = v.Struct(conf)
if err != nil {
log.Fatal().Err(err).Msg("Invalid config")
}
log.Logger = log.Level(zerolog.Level(utils.GetLogLevel(conf.LogLevel)))
log.Info().Str("version", strings.TrimSpace(config.Version)).Msg("Starting Tinyauth")
if log.Logger.GetLevel() == zerolog.TraceLevel {
log.Warn().Msg("Log level set to trace, this will log sensitive information!")
}
app := bootstrap.NewBootstrapApp(conf)
err = app.Setup()
if err != nil {
log.Fatal().Err(err).Msg("Failed to setup app")
}
}
func Run() {
rootCmd := newRootCmd()
rootCmd.Register()
root := rootCmd.GetCmd()
userCmd := &cobra.Command{
Use: "user",
Short: "User utilities",
Long: `Utilities for creating and verifying tinyauth compatible users.`,
}
totpCmd := &cobra.Command{
Use: "totp",
Short: "Totp utilities",
Long: `Utilities for creating and verifying totp codes.`,
}
newCreateUserCmd(userCmd).Register()
newVerifyUserCmd(userCmd).Register()
newGenerateTotpCmd(totpCmd).Register()
newVersionCmd(root).Register()
newHealthcheckCmd(root).Register()
root.AddCommand(userCmd)
root.AddCommand(totpCmd)
err := root.Execute()
if err != nil {
log.Fatal().Err(err).Msg("Failed to execute root command")
}
func init() {
// Add user command
rootCmd.AddCommand(userCmd.UserCmd())
// Add totp command
rootCmd.AddCommand(totpCmd.TotpCmd())
// Read environment variables
viper.AutomaticEnv()
// Flags
rootCmd.Flags().Int("port", 3000, "Port to run the server on.")
rootCmd.Flags().String("address", "0.0.0.0", "Address to bind the server to.")
rootCmd.Flags().String("secret", "", "Secret to use for the cookie.")
rootCmd.Flags().String("secret-file", "", "Path to a file containing the secret.")
rootCmd.Flags().String("app-url", "", "The tinyauth URL.")
rootCmd.Flags().String("users", "", "Comma separated list of users in the format username:hash.")
rootCmd.Flags().String("users-file", "", "Path to a file containing users in the format username:hash.")
rootCmd.Flags().Bool("cookie-secure", false, "Send cookie over secure connection only.")
rootCmd.Flags().String("github-client-id", "", "Github OAuth client ID.")
rootCmd.Flags().String("github-client-secret", "", "Github OAuth client secret.")
rootCmd.Flags().String("github-client-secret-file", "", "Github OAuth client secret file.")
rootCmd.Flags().String("google-client-id", "", "Google OAuth client ID.")
rootCmd.Flags().String("google-client-secret", "", "Google OAuth client secret.")
rootCmd.Flags().String("google-client-secret-file", "", "Google OAuth client secret file.")
rootCmd.Flags().String("generic-client-id", "", "Generic OAuth client ID.")
rootCmd.Flags().String("generic-client-secret", "", "Generic OAuth client secret.")
rootCmd.Flags().String("generic-client-secret-file", "", "Generic OAuth client secret file.")
rootCmd.Flags().String("generic-scopes", "", "Generic OAuth scopes.")
rootCmd.Flags().String("generic-auth-url", "", "Generic OAuth auth URL.")
rootCmd.Flags().String("generic-token-url", "", "Generic OAuth token URL.")
rootCmd.Flags().String("generic-user-url", "", "Generic OAuth user info URL.")
rootCmd.Flags().String("generic-name", "Generic", "Generic OAuth provider name.")
rootCmd.Flags().Bool("generic-skip-ssl", false, "Skip SSL verification for the generic OAuth provider.")
rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.")
rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.")
rootCmd.Flags().String("oauth-auto-redirect", "none", "Auto redirect to the specified OAuth provider if configured. (available providers: github, google, generic)")
rootCmd.Flags().Int("session-expiry", 86400, "Session (cookie) expiration time in seconds.")
rootCmd.Flags().Int("login-timeout", 300, "Login timeout in seconds after max retries reached (0 to disable).")
rootCmd.Flags().Int("login-max-retries", 5, "Maximum login attempts before timeout (0 to disable).")
rootCmd.Flags().Int("log-level", 1, "Log level.")
rootCmd.Flags().String("app-title", "Tinyauth", "Title of the app.")
rootCmd.Flags().String("forgot-password-message", "", "Message to show on the forgot password page.")
rootCmd.Flags().String("background-image", "/background.jpg", "Background image URL for the login page.")
rootCmd.Flags().String("ldap-address", "", "LDAP server address (e.g. ldap://localhost:389).")
rootCmd.Flags().String("ldap-bind-dn", "", "LDAP bind DN (e.g. uid=user,dc=example,dc=com).")
rootCmd.Flags().String("ldap-bind-password", "", "LDAP bind password.")
rootCmd.Flags().String("ldap-base-dn", "", "LDAP base DN (e.g. dc=example,dc=com).")
rootCmd.Flags().Bool("ldap-insecure", false, "Skip certificate verification for the LDAP server.")
rootCmd.Flags().String("ldap-search-filter", "(uid=%s)", "LDAP search filter for user lookup.")
// Bind flags to environment
viper.BindEnv("port", "PORT")
viper.BindEnv("address", "ADDRESS")
viper.BindEnv("secret", "SECRET")
viper.BindEnv("secret-file", "SECRET_FILE")
viper.BindEnv("app-url", "APP_URL")
viper.BindEnv("users", "USERS")
viper.BindEnv("users-file", "USERS_FILE")
viper.BindEnv("cookie-secure", "COOKIE_SECURE")
viper.BindEnv("github-client-id", "GITHUB_CLIENT_ID")
viper.BindEnv("github-client-secret", "GITHUB_CLIENT_SECRET")
viper.BindEnv("github-client-secret-file", "GITHUB_CLIENT_SECRET_FILE")
viper.BindEnv("google-client-id", "GOOGLE_CLIENT_ID")
viper.BindEnv("google-client-secret", "GOOGLE_CLIENT_SECRET")
viper.BindEnv("google-client-secret-file", "GOOGLE_CLIENT_SECRET_FILE")
viper.BindEnv("generic-client-id", "GENERIC_CLIENT_ID")
viper.BindEnv("generic-client-secret", "GENERIC_CLIENT_SECRET")
viper.BindEnv("generic-client-secret-file", "GENERIC_CLIENT_SECRET_FILE")
viper.BindEnv("generic-scopes", "GENERIC_SCOPES")
viper.BindEnv("generic-auth-url", "GENERIC_AUTH_URL")
viper.BindEnv("generic-token-url", "GENERIC_TOKEN_URL")
viper.BindEnv("generic-user-url", "GENERIC_USER_URL")
viper.BindEnv("generic-name", "GENERIC_NAME")
viper.BindEnv("generic-skip-ssl", "GENERIC_SKIP_SSL")
viper.BindEnv("disable-continue", "DISABLE_CONTINUE")
viper.BindEnv("oauth-whitelist", "OAUTH_WHITELIST")
viper.BindEnv("oauth-auto-redirect", "OAUTH_AUTO_REDIRECT")
viper.BindEnv("session-expiry", "SESSION_EXPIRY")
viper.BindEnv("log-level", "LOG_LEVEL")
viper.BindEnv("app-title", "APP_TITLE")
viper.BindEnv("login-timeout", "LOGIN_TIMEOUT")
viper.BindEnv("login-max-retries", "LOGIN_MAX_RETRIES")
viper.BindEnv("forgot-password-message", "FORGOT_PASSWORD_MESSAGE")
viper.BindEnv("background-image", "BACKGROUND_IMAGE")
viper.BindEnv("ldap-address", "LDAP_ADDRESS")
viper.BindEnv("ldap-bind-dn", "LDAP_BIND_DN")
viper.BindEnv("ldap-bind-password", "LDAP_BIND_PASSWORD")
viper.BindEnv("ldap-base-dn", "LDAP_BASE_DN")
viper.BindEnv("ldap-insecure", "LDAP_INSECURE")
viper.BindEnv("ldap-search-filter", "LDAP_SEARCH_FILTER")
// Bind flags to viper
viper.BindPFlags(rootCmd.Flags())
}

View File

@@ -0,0 +1,121 @@
package generate
import (
"errors"
"fmt"
"os"
"strings"
"tinyauth/internal/utils"
"github.com/charmbracelet/huh"
"github.com/mdp/qrterminal/v3"
"github.com/pquerna/otp/totp"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
// Interactive flag
var interactive bool
// Input user
var iUser string
var GenerateCmd = &cobra.Command{
Use: "generate",
Short: "Generate a totp secret",
Run: func(cmd *cobra.Command, args []string) {
// Setup logger
log.Logger = log.Level(zerolog.InfoLevel)
// Use simple theme
var baseTheme *huh.Theme = huh.ThemeBase()
// Interactive
if interactive {
// Create huh form
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().Title("Current username:hash").Value(&iUser).Validate((func(s string) error {
if s == "" {
return errors.New("user cannot be empty")
}
return nil
})),
),
)
// Run form
err := form.WithTheme(baseTheme).Run()
if err != nil {
log.Fatal().Err(err).Msg("Form failed")
}
}
// Parse user
user, err := utils.ParseUser(iUser)
if err != nil {
log.Fatal().Err(err).Msg("Failed to parse user")
}
// Check if user was using docker escape
dockerEscape := false
if strings.Contains(iUser, "$$") {
dockerEscape = true
}
// Check it has totp
if user.TotpSecret != "" {
log.Fatal().Msg("User already has a totp secret")
}
// Generate totp secret
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "Tinyauth",
AccountName: user.Username,
})
if err != nil {
log.Fatal().Err(err).Msg("Failed to generate totp secret")
}
// Create secret
secret := key.Secret()
// Print secret and image
log.Info().Str("secret", secret).Msg("Generated totp secret")
// Print QR code
log.Info().Msg("Generated QR code")
config := qrterminal.Config{
Level: qrterminal.L,
Writer: os.Stdout,
BlackChar: qrterminal.BLACK,
WhiteChar: qrterminal.WHITE,
QuietZone: 2,
}
qrterminal.GenerateWithConfig(key.URL(), config)
// Add the secret to the user
user.TotpSecret = secret
// If using docker escape re-escape it
if dockerEscape {
user.Password = strings.ReplaceAll(user.Password, "$", "$$")
}
// Print success
log.Info().Str("user", fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TotpSecret)).Msg("Add the totp secret to your authenticator app then use the verify command to ensure everything is working correctly.")
},
}
func init() {
// Add interactive flag
GenerateCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Run in interactive mode")
GenerateCmd.Flags().StringVar(&iUser, "user", "", "Your current username:hash")
}

22
cmd/totp/totp.go Normal file
View File

@@ -0,0 +1,22 @@
package cmd
import (
"tinyauth/cmd/totp/generate"
"github.com/spf13/cobra"
)
func TotpCmd() *cobra.Command {
// Create the totp command
totpCmd := &cobra.Command{
Use: "totp",
Short: "Totp utilities",
Long: `Utilities for creating and verifying totp codes.`,
}
// Add the generate command
totpCmd.AddCommand(generate.GenerateCmd)
// Return the totp command
return totpCmd
}

97
cmd/user/create/create.go Normal file
View File

@@ -0,0 +1,97 @@
package create
import (
"errors"
"fmt"
"strings"
"github.com/charmbracelet/huh"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"golang.org/x/crypto/bcrypt"
)
// Interactive flag
var interactive bool
// Docker flag
var docker bool
// i stands for input
var iUsername string
var iPassword string
var CreateCmd = &cobra.Command{
Use: "create",
Short: "Create a user",
Long: `Create a user either interactively or by passing flags.`,
Run: func(cmd *cobra.Command, args []string) {
// Setup logger
log.Logger = log.Level(zerolog.InfoLevel)
// Check if interactive
if interactive {
// Create huh form
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().Title("Username").Value(&iUsername).Validate((func(s string) error {
if s == "" {
return errors.New("username cannot be empty")
}
return nil
})),
huh.NewInput().Title("Password").Value(&iPassword).Validate((func(s string) error {
if s == "" {
return errors.New("password cannot be empty")
}
return nil
})),
huh.NewSelect[bool]().Title("Format the output for docker?").Options(huh.NewOption("Yes", true), huh.NewOption("No", false)).Value(&docker),
),
)
// Use simple theme
var baseTheme *huh.Theme = huh.ThemeBase()
err := form.WithTheme(baseTheme).Run()
if err != nil {
log.Fatal().Err(err).Msg("Form failed")
}
}
// Do we have username and password?
if iUsername == "" || iPassword == "" {
log.Fatal().Err(errors.New("error invalid input")).Msg("Username and password cannot be empty")
}
log.Info().Str("username", iUsername).Str("password", iPassword).Bool("docker", docker).Msg("Creating user")
// Hash password
password, err := bcrypt.GenerateFromPassword([]byte(iPassword), bcrypt.DefaultCost)
if err != nil {
log.Fatal().Err(err).Msg("Failed to hash password")
}
// Convert password to string
passwordString := string(password)
// Escape $ for docker
if docker {
passwordString = strings.ReplaceAll(passwordString, "$", "$$")
}
// Log user created
log.Info().Str("user", fmt.Sprintf("%s:%s", iUsername, passwordString)).Msg("User created")
},
}
func init() {
// Flags
CreateCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Create a user interactively")
CreateCmd.Flags().BoolVar(&docker, "docker", false, "Format output for docker")
CreateCmd.Flags().StringVar(&iUsername, "username", "", "Username")
CreateCmd.Flags().StringVar(&iPassword, "password", "", "Password")
}

24
cmd/user/user.go Normal file
View File

@@ -0,0 +1,24 @@
package cmd
import (
"tinyauth/cmd/user/create"
"tinyauth/cmd/user/verify"
"github.com/spf13/cobra"
)
func UserCmd() *cobra.Command {
// Create the user command
userCmd := &cobra.Command{
Use: "user",
Short: "User utilities",
Long: `Utilities for creating and verifying tinyauth compatible users.`,
}
// Add subcommands
userCmd.AddCommand(create.CreateCmd)
userCmd.AddCommand(verify.VerifyCmd)
// Return the user command
return userCmd
}

122
cmd/user/verify/verify.go Normal file
View File

@@ -0,0 +1,122 @@
package verify
import (
"errors"
"tinyauth/internal/utils"
"github.com/charmbracelet/huh"
"github.com/pquerna/otp/totp"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"golang.org/x/crypto/bcrypt"
)
// Interactive flag
var interactive bool
// Docker flag
var docker bool
// i stands for input
var iUsername string
var iPassword string
var iTotp string
var iUser string
var VerifyCmd = &cobra.Command{
Use: "verify",
Short: "Verify a user is set up correctly",
Long: `Verify a user is set up correctly meaning that it has a correct username, password and totp code.`,
Run: func(cmd *cobra.Command, args []string) {
// Setup logger
log.Logger = log.Level(zerolog.InfoLevel)
// Use simple theme
var baseTheme *huh.Theme = huh.ThemeBase()
// Check if interactive
if interactive {
// Create huh form
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().Title("User (username:hash:totp)").Value(&iUser).Validate((func(s string) error {
if s == "" {
return errors.New("user cannot be empty")
}
return nil
})),
huh.NewInput().Title("Username").Value(&iUsername).Validate((func(s string) error {
if s == "" {
return errors.New("username cannot be empty")
}
return nil
})),
huh.NewInput().Title("Password").Value(&iPassword).Validate((func(s string) error {
if s == "" {
return errors.New("password cannot be empty")
}
return nil
})),
huh.NewInput().Title("Totp Code (if setup)").Value(&iTotp),
),
)
// Run form
err := form.WithTheme(baseTheme).Run()
if err != nil {
log.Fatal().Err(err).Msg("Form failed")
}
}
// Parse user
user, err := utils.ParseUser(iUser)
if err != nil {
log.Fatal().Err(err).Msg("Failed to parse user")
}
// Compare username
if user.Username != iUsername {
log.Fatal().Msg("Username is incorrect")
}
// Compare password
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(iPassword))
if err != nil {
log.Fatal().Msg("Ppassword is incorrect")
}
// Check if user has 2fa code
if user.TotpSecret == "" {
if iTotp != "" {
log.Warn().Msg("User does not have 2fa secret")
}
log.Info().Msg("User verified")
return
}
// Check totp code
ok := totp.Validate(iTotp, user.TotpSecret)
if !ok {
log.Fatal().Msg("Totp code incorrect")
}
// Done
log.Info().Msg("User verified")
},
}
func init() {
// Flags
VerifyCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Create a user interactively")
VerifyCmd.Flags().BoolVar(&docker, "docker", false, "Is the user formatted for docker?")
VerifyCmd.Flags().StringVar(&iUsername, "username", "", "Username")
VerifyCmd.Flags().StringVar(&iPassword, "password", "", "Password")
VerifyCmd.Flags().StringVar(&iTotp, "totp", "", "Totp code")
VerifyCmd.Flags().StringVar(&iUser, "user", "", "Hash (username:hash:totp combination)")
}

View File

@@ -1,118 +0,0 @@
package cmd
import (
"errors"
"tinyauth/internal/utils"
"github.com/charmbracelet/huh"
"github.com/pquerna/otp/totp"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"golang.org/x/crypto/bcrypt"
)
type verifyUserCmd struct {
root *cobra.Command
cmd *cobra.Command
interactive bool
username string
password string
totp string
user string
}
func newVerifyUserCmd(root *cobra.Command) *verifyUserCmd {
return &verifyUserCmd{
root: root,
}
}
func (c *verifyUserCmd) Register() {
c.cmd = &cobra.Command{
Use: "verify",
Short: "Verify a user is set up correctly",
Long: `Verify a user is set up correctly meaning that it has a correct username, password and TOTP code.`,
Run: c.run,
}
c.cmd.Flags().BoolVarP(&c.interactive, "interactive", "i", false, "Validate a user interactively")
c.cmd.Flags().StringVar(&c.username, "username", "", "Username")
c.cmd.Flags().StringVar(&c.password, "password", "", "Password")
c.cmd.Flags().StringVar(&c.totp, "totp", "", "TOTP code")
c.cmd.Flags().StringVar(&c.user, "user", "", "Hash (username:hash:totp)")
if c.root != nil {
c.root.AddCommand(c.cmd)
}
}
func (c *verifyUserCmd) GetCmd() *cobra.Command {
return c.cmd
}
func (c *verifyUserCmd) run(cmd *cobra.Command, args []string) {
log.Logger = log.Level(zerolog.InfoLevel)
if c.interactive {
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().Title("User (username:hash:totp)").Value(&c.user).Validate((func(s string) error {
if s == "" {
return errors.New("user cannot be empty")
}
return nil
})),
huh.NewInput().Title("Username").Value(&c.username).Validate((func(s string) error {
if s == "" {
return errors.New("username cannot be empty")
}
return nil
})),
huh.NewInput().Title("Password").Value(&c.password).Validate((func(s string) error {
if s == "" {
return errors.New("password cannot be empty")
}
return nil
})),
huh.NewInput().Title("TOTP Code (optional)").Value(&c.totp),
),
)
var baseTheme *huh.Theme = huh.ThemeBase()
err := form.WithTheme(baseTheme).Run()
if err != nil {
log.Fatal().Err(err).Msg("Form failed")
}
}
user, err := utils.ParseUser(c.user)
if err != nil {
log.Fatal().Err(err).Msg("Failed to parse user")
}
if user.Username != c.username {
log.Fatal().Msg("Username is incorrect")
}
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(c.password))
if err != nil {
log.Fatal().Msg("Password is incorrect")
}
if user.TotpSecret == "" {
if c.totp != "" {
log.Warn().Msg("User does not have TOTP secret")
}
log.Info().Msg("User verified")
return
}
ok := totp.Validate(c.totp, user.TotpSecret)
if !ok {
log.Fatal().Msg("TOTP code incorrect")
}
log.Info().Msg("User verified")
}

View File

@@ -2,41 +2,23 @@ package cmd
import (
"fmt"
"tinyauth/internal/config"
"tinyauth/internal/constants"
"github.com/spf13/cobra"
)
type versionCmd struct {
root *cobra.Command
cmd *cobra.Command
// Create the version command
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number of Tinyauth",
Long: `All software has versions. This is Tinyauth's`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Version: %s\n", constants.Version)
fmt.Printf("Commit Hash: %s\n", constants.CommitHash)
fmt.Printf("Build Timestamp: %s\n", constants.BuildTimestamp)
},
}
func newVersionCmd(root *cobra.Command) *versionCmd {
return &versionCmd{
root: root,
}
}
func (c *versionCmd) Register() {
c.cmd = &cobra.Command{
Use: "version",
Short: "Print the version number of Tinyauth",
Long: `All software has versions. This is Tinyauth's.`,
Run: c.run,
}
if c.root != nil {
c.root.AddCommand(c.cmd)
}
}
func (c *versionCmd) GetCmd() *cobra.Command {
return c.cmd
}
func (c *versionCmd) run(cmd *cobra.Command, args []string) {
fmt.Printf("Version: %s\n", config.Version)
fmt.Printf("Commit Hash: %s\n", config.CommitHash)
fmt.Printf("Build Timestamp: %s\n", config.BuildTimestamp)
func init() {
rootCmd.AddCommand(versionCmd)
}

View File

@@ -1,8 +0,0 @@
coverage:
status:
project:
default:
informational: true
patch:
default:
informational: true

View File

@@ -13,8 +13,8 @@ services:
image: traefik/whoami:latest
labels:
traefik.enable: true
traefik.http.routers.whoami.rule: Host(`whoami.example.com`)
traefik.http.routers.whoami.middlewares: tinyauth
traefik.http.routers.nginx.rule: Host(`whoami.example.com`)
traefik.http.routers.nginx.middlewares: tinyauth
tinyauth-frontend:
container_name: tinyauth-frontend
@@ -34,17 +34,12 @@ services:
build:
context: .
dockerfile: Dockerfile.dev
args:
- VERSION=development
- COMMIT_HASH=development
- BUILD_TIMESTAMP=000-00-00T00:00:00Z
env_file: .env
volumes:
- ./internal:/tinyauth/internal
- ./cmd:/tinyauth/cmd
- ./main.go:/tinyauth/main.go
- /var/run/docker.sock:/var/run/docker.sock
- ./data:/data
ports:
- 3000:3000
- 4000:4000

View File

@@ -13,17 +13,16 @@ services:
image: traefik/whoami:latest
labels:
traefik.enable: true
traefik.http.routers.whoami.rule: Host(`whoami.example.com`)
traefik.http.routers.whoami.middlewares: tinyauth
traefik.http.routers.nginx.rule: Host(`whoami.example.com`)
traefik.http.routers.nginx.middlewares: tinyauth
tinyauth:
container_name: tinyauth
image: ghcr.io/steveiliop56/tinyauth:v3
environment:
- SECRET=some-random-32-chars-string
- APP_URL=https://tinyauth.example.com
- USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password
volumes:
- ./data:/data
labels:
traefik.enable: true
traefik.http.routers.tinyauth.rule: Host(`tinyauth.example.com`)

File diff suppressed because it is too large Load Diff

View File

@@ -8,11 +8,10 @@
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="Tinyauth" />
<meta name="robots" content="nofollow, noindex" />
<link rel="manifest" href="/site.webmanifest" />
<title>Tinyauth</title>
</head>
<body>
<body class="dark">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

View File

@@ -1,5 +1,5 @@
{
"name": "tinyauth",
"name": "tinyauth-shadcn",
"private": true,
"version": "0.0.0",
"type": "module",
@@ -7,53 +7,53 @@
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"tsc": "tsc -b"
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-query": "^5.90.12",
"axios": "^1.13.2",
"@hookform/resolvers": "^5.1.1",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.7",
"@tailwindcss/vite": "^4.1.11",
"@tanstack/react-query": "^5.82.0",
"axios": "^1.10.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"i18next": "^25.7.3",
"dompurify": "^3.2.6",
"i18next": "^25.3.2",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-resources-to-backend": "^1.2.1",
"input-otp": "^1.4.2",
"lucide-react": "^0.561.0",
"lucide-react": "^0.525.0",
"next-themes": "^0.4.6",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-hook-form": "^7.68.0",
"react-i18next": "^16.5.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.60.0",
"react-i18next": "^15.6.0",
"react-markdown": "^10.1.0",
"react-router": "^7.10.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
"zod": "^4.2.1"
"react-router": "^7.6.3",
"sonner": "^2.0.6",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.11",
"zod": "^4.0.2"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
"@tanstack/eslint-plugin-query": "^5.91.2",
"@types/node": "^24.10.2",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2",
"eslint": "^9.39.2",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.25",
"globals": "^16.5.0",
"prettier": "3.7.4",
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.50.0",
"vite": "^7.3.0"
"@eslint/js": "^9.30.1",
"@tanstack/eslint-plugin-query": "^5.81.2",
"@types/node": "^24.0.13",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0",
"eslint": "^9.30.1",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.3.0",
"prettier": "3.6.2",
"tw-animate-css": "^1.3.5",
"typescript": "~5.8.3",
"typescript-eslint": "^8.36.0",
"vite": "^7.0.4"
}
}
}

View File

@@ -5,8 +5,8 @@ export const App = () => {
const { isLoggedIn } = useUserContext();
if (isLoggedIn) {
return <Navigate to="/logout" replace />;
return <Navigate to="/logout" />;
}
return <Navigate to="/login" replace />;
return <Navigate to="/login" />;
};

View File

@@ -12,7 +12,6 @@ import {
} from "../ui/form";
import { Button } from "../ui/button";
import { loginSchema, LoginSchema } from "@/schemas/login-schema";
import z from "zod";
interface Props {
onSubmit: (data: LoginSchema) => void;
@@ -23,11 +22,6 @@ export const LoginForm = (props: Props) => {
const { onSubmit, loading } = props;
const { t } = useTranslation();
z.config({
customError: (iss) =>
iss.input === undefined ? t("fieldRequired") : t("invalidInput"),
});
const form = useForm<LoginSchema>({
resolver: zodResolver(loginSchema),
});
@@ -45,7 +39,6 @@ export const LoginForm = (props: Props) => {
<Input
placeholder={t("loginUsername")}
disabled={loading}
autoComplete="username"
{...field}
/>
</FormControl>
@@ -65,7 +58,6 @@ export const LoginForm = (props: Props) => {
placeholder={t("loginPassword")}
type="password"
disabled={loading}
autoComplete="current-password"
{...field}
/>
</FormControl>

View File

@@ -8,8 +8,6 @@ import {
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { totpSchema, TotpSchema } from "@/schemas/totp-schema";
import { useTranslation } from "react-i18next";
import z from "zod";
interface Props {
formId: string;
@@ -19,12 +17,6 @@ interface Props {
export const TotpForm = (props: Props) => {
const { formId, onSubmit, loading } = props;
const { t } = useTranslation();
z.config({
customError: (iss) =>
iss.input === undefined ? t("fieldRequired") : t("invalidInput"),
});
const form = useForm<TotpSchema>({
resolver: zodResolver(totpSchema),
@@ -39,13 +31,7 @@ export const TotpForm = (props: Props) => {
render={({ field }) => (
<FormItem>
<FormControl>
<InputOTP
maxLength={6}
disabled={loading}
{...field}
autoComplete="one-time-code"
autoFocus
>
<InputOTP maxLength={6} disabled={loading} {...field}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />

View File

@@ -18,10 +18,9 @@ export const LanguageSelector = () => {
setLanguage(option as SupportedLanguage);
i18n.changeLanguage(option as SupportedLanguage);
};
return (
<Select onValueChange={handleSelect} value={language}>
<SelectTrigger>
<SelectTrigger className="absolute top-5 right-5">
<SelectValue placeholder="Select language" />
</SelectTrigger>
<SelectContent>

View File

@@ -0,0 +1,23 @@
import { useAppContext } from "@/context/app-context";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
export const VersionTooltip = () => {
const { version, buildTimestamp, commitHash } = useAppContext();
return (
<Tooltip>
<TooltipTrigger className="absolute bottom-2 text-muted-foreground text-sm">
Tinyauth {version}
</TooltipTrigger>
<TooltipContent>
<p>Version: {version}</p>
<p>Build Timestamp: {buildTimestamp}</p>
<p>Commit Hash: {commitHash}</p>
</TooltipContent>
</Tooltip>
);
};

View File

@@ -1,56 +0,0 @@
import {
Card,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "../ui/card";
import { Button } from "../ui/button";
import { Trans, useTranslation } from "react-i18next";
import { useLocation } from "react-router";
interface Props {
onClick: () => void;
appUrl: string;
currentUrl: string;
}
export const DomainWarning = (props: Props) => {
const { onClick, appUrl, currentUrl } = props;
const { t } = useTranslation();
const { search } = useLocation();
const searchParams = new URLSearchParams(search);
const redirectUri = searchParams.get("redirect_uri");
return (
<Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm">
<CardHeader>
<CardTitle className="text-3xl">{t("domainWarningTitle")}</CardTitle>
<CardDescription>
<Trans
t={t}
i18nKey="domainWarningSubtitle"
values={{ appUrl, currentUrl }}
components={{ code: <code /> }}
/>
</CardDescription>
</CardHeader>
<CardFooter className="flex flex-col items-stretch gap-2">
<Button onClick={onClick} variant="warning">
{t("ignoreTitle")}
</Button>
<Button
onClick={() =>
window.location.assign(
`${appUrl}/login?redirect_uri=${encodeURIComponent(redirectUri || "")}`,
)
}
variant="outline"
>
{t("goToCorrectDomainTitle")}
</Button>
</CardFooter>
</Card>
);
};

View File

@@ -1,6 +1,6 @@
import type { SVGProps } from "react";
export function OAuthIcon(props: SVGProps<SVGSVGElement>) {
export function GenericIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,18 +0,0 @@
import type { SVGProps } from "react";
export function MicrosoftIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="2em"
height="2em"
viewBox="0 0 256 256"
{...props}
>
<path fill="#f1511b" d="M121.666 121.666H0V0h121.666z"></path>
<path fill="#80cc28" d="M256 121.666H134.335V0H256z"></path>
<path fill="#00adef" d="M121.663 256.002H0V134.336h121.663z"></path>
<path fill="#fbbc09" d="M256 256.002H134.335V134.336H256z"></path>
</svg>
);
}

View File

@@ -1,20 +0,0 @@
import type { SVGProps } from "react";
export function PocketIDIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
xmlSpace="preserve"
width={512}
height={512}
viewBox="0 0 512 512"
{...props}
>
<circle cx="256" cy="256" r="256" />
<path
d="M268.6 102.4c64.4 0 116.8 52.4 116.8 116.7 0 25.3-8 49.4-23 69.6-14.8 19.9-35 34.3-58.4 41.7l-6.5 2-15.5-76.2 4.3-2c14-6.7 23-21.1 23-36.6 0-22.4-18.2-40.6-40.6-40.6S228 195.2 228 217.6c0 15.5 9 29.8 23 36.6l4.2 2-25 153.4h-69.5V102.4z"
className="fill-white"
/>
</svg>
);
}

View File

@@ -1,26 +0,0 @@
import type { SVGProps } from "react";
export function TailscaleIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
xmlSpace="preserve"
width={512}
height={512}
viewBox="0 0 512 512"
{...props}
>
<path
className="opacity-80"
fill="currentColor"
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.2s28.6 63.9 63.8 63.9m191.6 0c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9m0 193.9c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9m189.2-193.9c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9"
/>
<path
d="M65.6 127.7c35.3 0 63.9-28.6 63.9-63.9S100.9 0 65.6 0 1.8 28.6 1.8 63.9s28.6 63.8 63.8 63.8m0 384.3c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.8 28.7-63.8 63.9S30.4 512 65.6 512m191.6-384.3c35.3 0 63.9-28.6 63.9-63.9S292.5 0 257.2 0s-63.9 28.6-63.9 63.9 28.6 63.8 63.9 63.8m189.2 0c35.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.9s28.6 63.8 63.9 63.8m0 384.3c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9"
className="opacity-20"
fill="currentColor"
/>
</svg>
);
}

View File

@@ -1,16 +1,10 @@
import { useAppContext } from "@/context/app-context";
import { LanguageSelector } from "../language/language";
import { LanguageSelector } from "../components/language-selector";
import { Outlet } from "react-router";
import { useCallback, useEffect, useState } from "react";
import { DomainWarning } from "../domain-warning/domain-warning";
import { ThemeToggle } from "../theme-toggle/theme-toggle";
import { VersionTooltip } from "../components/version-tooltip";
const BaseLayout = ({ children }: { children: React.ReactNode }) => {
const { backgroundImage, title } = useAppContext();
useEffect(() => {
document.title = title;
}, [title]);
export const Layout = () => {
const { backgroundImage } = useAppContext();
return (
<div
@@ -21,42 +15,9 @@ const BaseLayout = ({ children }: { children: React.ReactNode }) => {
backgroundPosition: "center",
}}
>
<div className="absolute top-5 right-5 flex flex-row gap-2">
<ThemeToggle />
<LanguageSelector />
</div>
{children}
<LanguageSelector />
<Outlet />
<VersionTooltip />
</div>
);
};
export const Layout = () => {
const { appUrl, disableUiWarnings } = useAppContext();
const [ignoreDomainWarning, setIgnoreDomainWarning] = useState(() => {
return window.sessionStorage.getItem("ignoreDomainWarning") === "true";
});
const currentUrl = window.location.origin;
const handleIgnore = useCallback(() => {
window.sessionStorage.setItem("ignoreDomainWarning", "true");
setIgnoreDomainWarning(true);
}, [setIgnoreDomainWarning]);
if (!ignoreDomainWarning && !disableUiWarnings && appUrl !== currentUrl) {
return (
<BaseLayout>
<DomainWarning
appUrl={appUrl}
currentUrl={currentUrl}
onClick={() => handleIgnore()}
/>
</BaseLayout>
);
}
return (
<BaseLayout>
<Outlet />
</BaseLayout>
);
};

View File

@@ -1,73 +0,0 @@
import { createContext, useContext, useEffect, useState } from "react";
type Theme = "dark" | "light" | "system";
type ThemeProviderProps = {
children: React.ReactNode;
defaultTheme?: Theme;
storageKey?: string;
};
type ThemeProviderState = {
theme: Theme;
setTheme: (theme: Theme) => void;
};
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
};
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "vite-ui-theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
);
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove("light", "dark");
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
root.classList.add(systemTheme);
return;
}
root.classList.add(theme);
}, [theme]);
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme);
setTheme(theme);
},
};
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
);
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext);
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider");
return context;
};

View File

@@ -1,40 +0,0 @@
import { Moon, Sun } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useTheme } from "@/components/providers/theme-provider";
export function ThemeToggle() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="bg-card text-card-foreground hover:bg-card/90"
size="icon"
>
<Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -6,7 +6,7 @@ import { cn } from "@/lib/utils";
import { Loader2 } from "lucide-react";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:cursor-pointer",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
@@ -22,7 +22,7 @@ const buttonVariants = cva(
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
warning:
"bg-amber-500 text-white shadow-xs hover:bg-amber-400 focus-visible:ring-amber-200/20 dark:focus-visible:ring-amber-400/40",
"bg-amber-500 text-white shadow-xs hover:bg-amber-400 focus-visible:ring-amber-200/20 dark:focus-visible:ring-amber-400/40 dark:bg-amber-600",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",

View File

@@ -1,255 +0,0 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}

View File

@@ -35,7 +35,7 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
"hover:cursor-pointer border-input data-[placeholder]:text-card-foreground [&_svg:not([class*='text-'])]:text-card-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex w-fit items-center justify-between gap-2 rounded-md border bg-card hover:bg-card/90 px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-card dark:hover:bg-card/90 flex w-fit items-center justify-between gap-2 rounded-md border bg-card hover:bg-card/90 px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}

View File

@@ -1,8 +1,8 @@
import { useTheme } from "../providers/theme-provider";
import { useTheme } from "next-themes";
import { Toaster as Sonner, ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {
const { theme } = useTheme();
const { theme = "system" } = useTheme();
return (
<Sonner

View File

@@ -0,0 +1,59 @@
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
);
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
);
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-card text-muted-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className,
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-card fill-card z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View File

@@ -15,7 +15,7 @@ export const AppContextProvider = ({
}) => {
const { isFetching, data, error } = useSuspenseQuery({
queryKey: ["app"],
queryFn: () => axios.get("/api/context/app").then((res) => res.data),
queryFn: () => axios.get("/api/app").then((res) => res.data),
});
if (error && !isFetching) {

View File

@@ -15,7 +15,7 @@ export const UserContextProvider = ({
}) => {
const { isFetching, data, error } = useSuspenseQuery({
queryKey: ["user"],
queryFn: () => axios.get("/api/context/user").then((res) => res.data),
queryFn: () => axios.get("/api/user").then((res) => res.data),
});
if (error && !isFetching) {

View File

@@ -156,7 +156,7 @@ ul {
}
code {
@apply relative rounded bg-muted px-[0.2rem] py-[0.1rem] font-mono text-sm font-semibold break-all;
@apply relative rounded bg-muted px-[0.2rem] py-[0.1rem] font-mono text-sm font-semibold;
}
.lead {

View File

@@ -0,0 +1,15 @@
import { useCallback, useEffect, useRef } from "react";
export function useIsMounted(): () => boolean {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
return useCallback(() => isMounted.current, []);
}

View File

@@ -2,7 +2,6 @@ import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import resourcesToBackend from "i18next-resources-to-backend";
import { languages } from "./locales";
i18n
.use(LanguageDetector)
@@ -15,12 +14,12 @@ i18n
.init({
fallbackLng: "en",
debug: import.meta.env.MODE === "development",
nonExplicitSupportedLngs: true,
supportedLngs: Object.keys(languages),
load: "currentOnly",
detection: {
lookupLocalStorage: "tinyauth-lang",
interpolation: {
escapeValue: false,
},
load: "currentOnly",
});
export default i18n;

View File

@@ -18,8 +18,8 @@ export const languages = {
"nl-NL": "Nederlands",
"no-NO": "Norsk",
"pl-PL": "Polski",
"pt-BR": "Português (Brasil)",
"pt-PT": "Português (Portugal)",
"pt-BR": "Português",
"pt-PT": "Português",
"ro-RO": "Română",
"ru-RU": "Русский",
"sr-SP": "Српски",
@@ -28,7 +28,7 @@ export const languages = {
"uk-UA": "Українська",
"vi-VN": "Tiếng Việt",
"zh-CN": "简体中文",
"zh-TW": "繁體中文",
"zh-TW": "繁體中文(台灣)",
};
export type SupportedLanguage = keyof typeof languages;

View File

@@ -14,17 +14,14 @@
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Continue",
"continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectManually": "Redirect me manually",
"continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"continueTitle": "Continue",
"continueSubtitle": "Click the button to continue to your app.",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out",
@@ -47,16 +44,11 @@
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again",
"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?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
}

View File

@@ -14,17 +14,14 @@
"loginOauthFailSubtitle": "أخفق الحصول على رابط OAuth",
"loginOauthSuccessTitle": "إعادة توجيه",
"loginOauthSuccessSubtitle": "إعادة توجيه إلى مزود OAuth الخاص بك",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "متابعة",
"continueRedirectingTitle": "إعادة توجيه...",
"continueRedirectingSubtitle": "يجب إعادة توجيهك إلى التطبيق قريبا",
"continueRedirectManually": "Redirect me manually",
"continueInvalidRedirectTitle": "إعادة توجيه غير صالحة",
"continueInvalidRedirectSubtitle": "رابط إعادة التوجيه غير صالح",
"continueInsecureRedirectTitle": "إعادة توجيه غير آمنة",
"continueInsecureRedirectSubtitle": "أنت تحاول إعادة التوجيه من <code>https</code> إلى <code>http</code>، هل أنت متأكد أنك تريد المتابعة؟",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"continueTitle": "متابعة",
"continueSubtitle": "انقر الزر للمتابعة إلى التطبيق الخاص بك.",
"logoutFailTitle": "فشل تسجيل الخروج",
"logoutFailSubtitle": "يرجى إعادة المحاولة",
"logoutSuccessTitle": "تم تسجيل الخروج",
@@ -47,16 +44,11 @@
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "حاول مجددا",
"untrustedRedirectTitle": "إعادة توجيه غير موثوقة",
"untrustedRedirectSubtitle": "أنت تحاول إعادة التوجيه إلى نطاق لا يتطابق مع النطاق المكون الخاص بك (<code>{{domain}}</code>). هل أنت متأكد من أنك تريد المتابعة؟",
"cancelTitle": "إلغاء",
"forgotPasswordTitle": "نسيت كلمة المرور؟",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "حدث خطأ",
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "تجاهل",
"goToCorrectDomainTitle": "Go to correct domain"
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
}

View File

@@ -14,17 +14,14 @@
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Continue",
"continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectManually": "Redirect me manually",
"continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"continueTitle": "Continue",
"continueSubtitle": "Click the button to continue to your app.",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out",
@@ -47,16 +44,11 @@
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again",
"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?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
}

View File

@@ -1,62 +1,54 @@
{
"loginTitle": "Vítejte zpět, přihlaste se pomocí",
"loginTitleSimple": "Vítejte zpět, přihlaste se prosím",
"loginDivider": "Nebo",
"loginUsername": "Uživatelské jméno",
"loginPassword": "Heslo",
"loginSubmit": "Přihlásit",
"loginFailTitle": "Přihlášení se nezdařilo",
"loginFailSubtitle": "Zkontrolujte prosím své uživatelské jméno a heslo",
"loginFailRateLimit": "Přiliš mnoho neúspěšných pokusů přihlášení. Zkuste to prosím později",
"loginSuccessTitle": "Přihlášen",
"loginSuccessSubtitle": "Vítejte zpět!",
"loginOauthFailTitle": "Došlo k chybě",
"loginOauthFailSubtitle": "Nepodařilo se získat OAuth URL",
"loginOauthSuccessTitle": "Přesměrování",
"loginOauthSuccessSubtitle": "Přesměrování k poskytovateli OAuth",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Pokračovat",
"continueRedirectingTitle": "Přesměrování...",
"continueRedirectingSubtitle": "Brzy budete přesměrováni do aplikace",
"continueRedirectManually": "Redirect me manually",
"continueInsecureRedirectTitle": "Nezabezpečené přesměrování",
"continueInsecureRedirectSubtitle": "Pokoušíte se přesměrovat z <code>https</code> na <code>http</code>, které není bezpečné. Opravdu chcete pokračovat?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"logoutFailTitle": "Odhlášení se nezdařilo",
"logoutFailSubtitle": "Zkuste to prosím znovu",
"logoutSuccessTitle": "Odhlášen",
"logoutSuccessSubtitle": "Byl jste odhlášen",
"logoutTitle": "Odhlásit",
"logoutUsernameSubtitle": "Jste přihlášen jako <code>{{username}}</code>. Pro odhlášení klikněte na tlačítko níže.",
"logoutOauthSubtitle": "Jste přihlášen jako <code>{{username}}</code> pomocí {{provider}} poskytovatele OAuth. Pro odhlášení klikněte na tlačítko níže.",
"notFoundTitle": "Stránka nenalezena",
"notFoundSubtitle": "Stránka, kterou hledáte, neexistuje.",
"notFoundButton": "Jít domů",
"totpFailTitle": "Nepodařilo se ověřit kód",
"totpFailSubtitle": "Zkontrolujte prosím kód a zkuste to znovu",
"totpSuccessTitle": "Ověřeno",
"totpSuccessSubtitle": "Přesměrování do aplikace",
"totpTitle": "Zadejte TOTP kód",
"totpSubtitle": "Zadejte prosím kód z ověřovací aplikace.",
"unauthorizedTitle": "Nepovoleno",
"unauthorizedResourceSubtitle": "Uživatel s uživatelským jménem <code>{{username}}</code> není oprávněn k přístupu ke zdroji <code>{{resource}}</code>.",
"unauthorizedLoginSubtitle": "Uživatel s uživatelským jménem <code>{{username}}</code> není oprávněn k přihlášení.",
"unauthorizedGroupsSubtitle": "Uživatel s uživatelským jménem <code>{{username}}</code> není ve skupině potřebné k přístupu ke zdroji <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Vaše IP adresa <code>{{ip}}</code> není oprávněna k přístupu ke zdroji <code>{{resource}}</code>.",
"unauthorizedButton": "Zkusit znovu",
"cancelTitle": "Zrušit",
"forgotPasswordTitle": "Zapomněli jste heslo?",
"failedToFetchProvidersTitle": "Nepodařilo se načíst poskytovatele ověřování. Zkontrolujte prosím konfiguraci.",
"errorTitle": "Došlo k chybě",
"errorSubtitle": "Nastala chyba při pokusu o provedení této akce. Pro více informací prosím zkontrolujte konzolu.",
"forgotPasswordMessage": "Heslo můžete obnovit změnou proměnné `USERS`.",
"fieldRequired": "Toto pole je povinné",
"invalidInput": "Neplatný údaj",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
"loginTitle": "Welcome back, login with",
"loginTitleSimple": "Welcome back, please login",
"loginDivider": "Or",
"loginUsername": "Username",
"loginPassword": "Password",
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times. Please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "An error occurred",
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueTitle": "Continue",
"continueSubtitle": "Click the button to continue to your app.",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out",
"logoutSuccessSubtitle": "You have been logged out",
"logoutTitle": "Logout",
"logoutUsernameSubtitle": "You are currently logged in as <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.",
"notFoundTitle": "Page not found",
"notFoundSubtitle": "The page you are looking for does not exist.",
"notFoundButton": "Go home",
"totpFailTitle": "Failed to verify code",
"totpFailSubtitle": "Please check your code and try again",
"totpSuccessTitle": "Verified",
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"totpSubtitle": "Please enter the code from your authenticator app.",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again",
"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?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
}

View File

@@ -14,17 +14,14 @@
"loginOauthFailSubtitle": "Kunne ikke hente OAuth-URL",
"loginOauthSuccessTitle": "Omdirigerer",
"loginOauthSuccessSubtitle": "Omdirigerer til din OAuth-udbyder",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Fortsæt",
"continueRedirectingTitle": "Omdirigerer...",
"continueRedirectingSubtitle": "Du bør blive omdirigeret til appen snart",
"continueRedirectManually": "Redirect me manually",
"continueInvalidRedirectTitle": "Ugyldig omdirigering",
"continueInvalidRedirectSubtitle": "Omdirigerings-URL'en er ugyldig",
"continueInsecureRedirectTitle": "Usikker omdirigering",
"continueInsecureRedirectSubtitle": "Du forsøger at omdirigere fra <code>https</code> til <code>http</code>, som ikke er sikker. Er du sikker på, at du vil fortsætte?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"continueTitle": "Fortsæt",
"continueSubtitle": "Klik på knappen for at fortsætte til din app.",
"logoutFailTitle": "Log ud mislykkedes",
"logoutFailSubtitle": "Prøv venligst igen",
"logoutSuccessTitle": "Logget ud",
@@ -47,16 +44,11 @@
"unauthorizedGroupsSubtitle": "Brugeren med brugernavnet <code>{{username}}</code> er ikke i de grupper, som ressourcen <code>{{resource}}</code> kræver.",
"unauthorizedIpSubtitle": "Din IP adresse <code>{{ip}}</code> er ikke autoriseret til at tilgå ressourcen <code>{{resource}}</code>.",
"unauthorizedButton": "Prøv igen",
"untrustedRedirectTitle": "Usikker omdirigering",
"untrustedRedirectSubtitle": "Du forsøger at omdirigere til et domæne, der ikke matcher dit konfigurerede domæne (<code>{{domain}}</code>). Er du sikker på, at du vil fortsætte?",
"cancelTitle": "Annuller",
"forgotPasswordTitle": "Glemt din adgangskode?",
"failedToFetchProvidersTitle": "Kunne ikke indlæse godkendelsesudbydere. Tjek venligst din konfiguration.",
"errorTitle": "Der opstod en fejl",
"errorSubtitle": "Der opstod en fejl under forsøget på at udføre denne handling. Tjek venligst konsollen for mere information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
"errorSubtitle": "Der opstod en fejl under forsøget på at udføre denne handling. Tjek venligst konsollen for mere information."
}

View File

@@ -14,17 +14,14 @@
"loginOauthFailSubtitle": "Fehler beim Abrufen der OAuth-URL",
"loginOauthSuccessTitle": "Leite weiter",
"loginOauthSuccessSubtitle": "Weiterleitung zu Ihrem OAuth-Provider",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Weiter",
"continueRedirectingTitle": "Leite weiter...",
"continueRedirectingSubtitle": "Sie sollten in Kürze zur App weitergeleitet werden",
"continueRedirectManually": "Redirect me manually",
"continueInvalidRedirectTitle": "Ungültige Weiterleitung",
"continueInvalidRedirectSubtitle": "Die Weiterleitungs-URL ist ungültig",
"continueInsecureRedirectTitle": "Unsichere Weiterleitung",
"continueInsecureRedirectSubtitle": "Sie versuchen von <code>https</code> auf <code>http</code> weiterzuleiten, was unsicher ist. Sind Sie sicher, dass Sie fortfahren möchten?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"continueTitle": "Weiter",
"continueSubtitle": "Klicken Sie auf den Button, um zur App zu gelangen.",
"logoutFailTitle": "Abmelden fehlgeschlagen",
"logoutFailSubtitle": "Bitte versuchen Sie es erneut",
"logoutSuccessTitle": "Abgemeldet",
@@ -34,7 +31,7 @@
"logoutOauthSubtitle": "Sie sind derzeit als <code>{{username}}</code> über den OAuth-Anbieter {{provider}} angemeldet. Klicken Sie auf den Button unten, um sich abzumelden.",
"notFoundTitle": "Seite nicht gefunden",
"notFoundSubtitle": "Die gesuchte Seite existiert nicht.",
"notFoundButton": "Zurück",
"notFoundButton": "Nach Hause",
"totpFailTitle": "Fehler beim Verifizieren des Codes",
"totpFailSubtitle": "Bitte überprüfen Sie Ihren Code und versuchen Sie es erneut",
"totpSuccessTitle": "Verifiziert",
@@ -47,16 +44,11 @@
"unauthorizedGroupsSubtitle": "Der Benutzer mit Benutzername <code>{{username}}</code> ist nicht in den Gruppen, die von der Ressource <code>{{resource}}</code> benötigt werden.",
"unauthorizedIpSubtitle": "Ihre IP-Adresse <code>{{ip}}</code> ist nicht berechtigt, auf die Ressource <code>{{resource}}</code> zuzugreifen.",
"unauthorizedButton": "Erneut versuchen",
"untrustedRedirectTitle": "Nicht vertrauenswürdige Weiterleitung",
"untrustedRedirectSubtitle": "Sie versuchen auf eine Domain umzuleiten, die nicht mit Ihrer konfigurierten Domain übereinstimmt (<code>{{domain}}</code>). Sind Sie sicher, dass Sie fortfahren möchten?",
"cancelTitle": "Abbrechen",
"forgotPasswordTitle": "Passwort vergessen?",
"failedToFetchProvidersTitle": "Fehler beim Laden der Authentifizierungsanbieter. Bitte überprüfen Sie Ihre Konfiguration.",
"errorTitle": "Ein Fehler ist aufgetreten",
"errorSubtitle": "Beim Versuch, diese Aktion auszuführen, ist ein Fehler aufgetreten. Bitte überprüfen Sie die Konsole für weitere Informationen.",
"forgotPasswordMessage": "Das Passwort kann durch Änderung der 'USERS' Variable zurückgesetzt werden.",
"fieldRequired": "Dieses Feld ist notwendig",
"invalidInput": "Ungültige Eingabe",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
"errorSubtitle": "Beim Versuch, diese Aktion auszuführen, ist ein Fehler aufgetreten. Bitte überprüfen Sie die Konsole für weitere Informationen."
}

View File

@@ -14,17 +14,14 @@
"loginOauthFailSubtitle": "Αποτυχία λήψης OAuth URL",
"loginOauthSuccessTitle": "Ανακατεύθυνση",
"loginOauthSuccessSubtitle": "Ανακατεύθυνση στον πάροχο OAuth σας",
"loginOauthAutoRedirectTitle": "Αυτόματη Ανακατεύθυνση OAuth",
"loginOauthAutoRedirectSubtitle": "Θα ανακατευθυνθείτε αυτόματα στον πάροχο OAuth σας για να επαληθευτείτε.",
"loginOauthAutoRedirectButton": "Ανακατεύθυνση τώρα",
"continueTitle": "Συνέχεια",
"continueRedirectingTitle": "Ανακατεύθυνση...",
"continueRedirectingSubtitle": "Θα πρέπει να μεταφερθείτε σύντομα στην εφαρμογή σας",
"continueRedirectManually": "Χειροκίνητη ανακατεύθυνση",
"continueInvalidRedirectTitle": "Μη έγκυρη ανακατεύθυνση",
"continueInvalidRedirectSubtitle": "Το URL ανακατεύθυνσης δεν είναι έγκυρο",
"continueInsecureRedirectTitle": "Μη ασφαλής ανακατεύθυνση",
"continueInsecureRedirectSubtitle": "Προσπαθείτε να ανακατευθύνετε από <code>https</code> σε <code>http</code> το οποίο δεν είναι ασφαλές. Είστε σίγουροι ότι θέλετε να συνεχίσετε;",
"continueUntrustedRedirectTitle": "Μη έμπιστη ανακατεύθυνση",
"continueUntrustedRedirectSubtitle": "Προσπαθείτε να ανακατευθύνετε σε ένα domain που δεν ταιριάζει με το ρυθμισμένο domain σας (<code>{{cookieDomain}}</code>). Είστε βέβαιοι ότι θέλετε να συνεχίσετε;",
"continueTitle": "Συνέχεια",
"continueSubtitle": "Κάντε κλικ στο κουμπί για να συνεχίσετε στην εφαρμογή σας.",
"logoutFailTitle": "Αποτυχία αποσύνδεσης",
"logoutFailSubtitle": "Παρακαλώ δοκιμάστε ξανά",
"logoutSuccessTitle": "Αποσυνδεδεμένος",
@@ -47,16 +44,11 @@
"unauthorizedGroupsSubtitle": "Ο χρήστης με όνομα χρήστη <code>{{username}}</code> δεν είναι στις ομάδες που απαιτούνται από τον πόρο <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Η διεύθυνση IP σας <code>{{ip}}</code> δεν είναι εξουσιοδοτημένη να έχει πρόσβαση στον πόρο <code>{{resource}}</code>.",
"unauthorizedButton": "Προσπαθήστε ξανά",
"untrustedRedirectTitle": "Μη έμπιστη ανακατεύθυνση",
"untrustedRedirectSubtitle": "Προσπαθείτε να ανακατευθύνετε σε ένα domain που δεν ταιριάζει με τον ρυθμισμένο domain σας (<code>{{domain}}</code>). Είστε βέβαιοι ότι θέλετε να συνεχίσετε;",
"cancelTitle": "Ακύρωση",
"forgotPasswordTitle": "Ξεχάσατε το συνθηματικό σας;",
"failedToFetchProvidersTitle": "Αποτυχία φόρτωσης παρόχων πιστοποίησης. Παρακαλώ ελέγξτε τις ρυθμίσεις σας.",
"errorTitle": "Παρουσιάστηκε ένα σφάλμα",
"errorSubtitle": "Παρουσιάστηκε σφάλμα κατά την προσπάθεια εκτέλεσης αυτής της ενέργειας. Ελέγξτε την κονσόλα για περισσότερες πληροφορίες.",
"forgotPasswordMessage": "Μπορείτε να επαναφέρετε τον κωδικό πρόσβασής σας αλλάζοντας τη μεταβλητή περιβάλλοντος `USERS`.",
"fieldRequired": "Αυτό το πεδίο είναι υποχρεωτικό",
"invalidInput": "Μη έγκυρη καταχώρηση",
"domainWarningTitle": "Μη έγκυρο domain",
"domainWarningSubtitle": "Αυτή η εφαρμογή έχει ρυθμιστεί για πρόσβαση από <code>{{appUrl}}</code>, αλλά <code>{{currentUrl}}</code> χρησιμοποιείται. Αν συνεχίσετε, μπορεί να αντιμετωπίσετε προβλήματα με την ταυτοποίηση.",
"ignoreTitle": "Παράβλεψη",
"goToCorrectDomainTitle": "Μεταβείτε στο σωστό domain"
"errorSubtitle": "Παρουσιάστηκε σφάλμα κατά την προσπάθεια εκτέλεσης αυτής της ενέργειας. Ελέγξτε την κονσόλα για περισσότερες πληροφορίες."
}

View File

@@ -14,17 +14,14 @@
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Continue",
"continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectManually": "Redirect me manually",
"continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"continueTitle": "Continue",
"continueSubtitle": "Click the button to continue to your app.",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out",
@@ -47,16 +44,12 @@
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again",
"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?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable."
}

View File

@@ -14,17 +14,14 @@
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Continue",
"continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectManually": "Redirect me manually",
"continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"continueTitle": "Continue",
"continueSubtitle": "Click the button to continue to your app.",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out",
@@ -47,16 +44,12 @@
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again",
"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?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable."
}

View File

@@ -14,17 +14,14 @@
"loginOauthFailSubtitle": "Error al obtener la URL de OAuth",
"loginOauthSuccessTitle": "Redireccionando",
"loginOauthSuccessSubtitle": "Redireccionando a tu proveedor de OAuth",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Continuar",
"continueRedirectingTitle": "Redireccionando...",
"continueRedirectingSubtitle": "Pronto será redirigido a la aplicación",
"continueRedirectManually": "Redirect me manually",
"continueInvalidRedirectTitle": "Redirección inválida",
"continueInvalidRedirectSubtitle": "La URL de redirección es inválida",
"continueInsecureRedirectTitle": "Redirección insegura",
"continueInsecureRedirectSubtitle": "Está intentando redirigir desde <code>https</code> a <code>http</code> lo cual no es seguro. ¿Está seguro que desea continuar?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"continueTitle": "Continuar",
"continueSubtitle": "Haga clic en el botón para continuar hacia su aplicación.",
"logoutFailTitle": "Fallo al cerrar sesión",
"logoutFailSubtitle": "Por favor intente nuevamente",
"logoutSuccessTitle": "Sesión cerrada",
@@ -47,16 +44,11 @@
"unauthorizedGroupsSubtitle": "El usuario con nombre de usuario <code>{{username}}</code> no está en los grupos requeridos por el recurso <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Inténtelo de nuevo",
"untrustedRedirectTitle": "Redirección no confiable",
"untrustedRedirectSubtitle": "Está intentando redirigir a un dominio que no coincide con su dominio configurado (<code>{{domain}}</code>). ¿Está seguro que desea continuar?",
"cancelTitle": "Cancelar",
"forgotPasswordTitle": "¿Olvidó su contraseña?",
"failedToFetchProvidersTitle": "Error al cargar los proveedores de autenticación. Por favor revise su configuración.",
"errorTitle": "Ha ocurrido un error",
"errorSubtitle": "Ocurrió un error mientras se trataba de realizar esta acción. Por favor, revise la consola para más información.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
"errorSubtitle": "Ocurrió un error mientras se trataba de realizar esta acción. Por favor, revise la consola para más información."
}

View File

@@ -1,62 +1,54 @@
{
"loginTitle": "Tervetuloa takaisin, kirjaudu sisään käyttäen",
"loginTitleSimple": "Tervetuloa takaisin, ole hyvä ja kirjaudu",
"loginDivider": "Tai",
"loginUsername": "Käyttäjätunnus",
"loginPassword": "Salasana",
"loginSubmit": "Kirjaudu",
"loginFailTitle": "Kirjautuminen epäonnistui",
"loginFailSubtitle": "Tarkista käyttäjätunnuksesi ja salasanasi",
"loginFailRateLimit": "Kirjautuminen epäonnistui liian monta kertaa. Yritä myöhemmin uudelleen",
"loginSuccessTitle": "Olet kirjautunut sisään",
"loginSuccessSubtitle": "Tervetuloa takaisin!",
"loginOauthFailTitle": "Tapahtui virhe",
"loginOauthFailSubtitle": "OAuthin URL-osoitteen haku epäonnistui",
"loginOauthSuccessTitle": "Uudelleenohjataan",
"loginOauthSuccessSubtitle": "Uudelleenohjaus OAuth -palveluntarjoajallesi",
"loginOauthAutoRedirectTitle": "Automaattinen OAuth -uudelleenohjaus",
"loginOauthAutoRedirectSubtitle": "Sinut ohjataan automaattisesti OAuth -palveluntarjoajallesi todentamista varten.",
"loginOauthAutoRedirectButton": "Siirry nyt",
"continueTitle": "Jatka",
"continueRedirectingTitle": "Uudelleenohjataan...",
"continueRedirectingSubtitle": "Sinun pitäisi ohjautua sovellukseen pian",
"continueRedirectManually": "Siirrä minut manuaalisesti",
"continueInsecureRedirectTitle": "Turvaton uudelleenohjaus",
"continueInsecureRedirectSubtitle": "Yrität siirtyä suojatusta <code>https</code> -sivusta suojaamattomalle <code>http</code> -sivulle. Oletko varma, että haluat jatkaa?",
"continueUntrustedRedirectTitle": "Ei-luotettu uudelleenohjaus",
"continueUntrustedRedirectSubtitle": "Yrität uudelleenohjata domainiin, joka ei vastaa määritettyä verkkotunnusta (<code>{{cookieDomain}}</code>). Oletko varma, että haluat jatkaa?",
"logoutFailTitle": "Uloskirjautuminen epäonnistui",
"logoutFailSubtitle": "Ole hyvä ja yritä uudelleen",
"logoutSuccessTitle": "Kirjauduttu ulos",
"logoutSuccessSubtitle": "Sinut on kirjattu ulos",
"logoutTitle": "Kirjaudu ulos",
"logoutUsernameSubtitle": "Olet kirjautuneena sisään tunnuksella <code>{{username}}</code>. Kirjaudu ulos alla olevasta painikkeesta.",
"logoutOauthSubtitle": "Olet kirjautuneena sisään tunnuksella <code>{{username}}</code> OAuth palvelun {{provider}} kautta. Kirjaudu ulos alla olevasta painikkeesta.",
"notFoundTitle": "Sivua ei löydy",
"notFoundSubtitle": "Sivua, jota etsit ei ole olemassa.",
"notFoundButton": "Palaa kotinäkymään",
"totpFailTitle": "Koodin vahvistus epäonnistui",
"totpFailSubtitle": "Tarkista koodisi ja yritä uudelleen",
"totpSuccessTitle": "Vahvistettu",
"totpSuccessSubtitle": "Uudelleenohjataan sovelluksellesi",
"totpTitle": "Syötä TOTP -koodisi",
"totpSubtitle": "Ole hyvä ja syötä koodi todennussovelluksestasi.",
"unauthorizedTitle": "Ei sallittu",
"unauthorizedResourceSubtitle": "Käyttäjällä <code>{{username}}</code> ei ole pääsyä kohteeseen <code>{{resource}}</code>.",
"unauthorizedLoginSubtitle": "Käyttäjällä <code>{{username}}</code> ei ole lupaa kirjautua.",
"unauthorizedGroupsSubtitle": "Käyttäjä <code>{{username}}</code> ei ole ryhmässä, joka vaaditaan pääsyyn kohteeseen <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "IP osoitteestasi <code>{{ip}}</code> ei ole pääsyä kohteeseen <code>{{resource}}</code>.",
"unauthorizedButton": "Yritä uudelleen",
"cancelTitle": "Peruuta",
"forgotPasswordTitle": "Unohditko salasanasi?",
"failedToFetchProvidersTitle": "Todennuspalvelujen tarjoajien lataaminen epäonnistui. Tarkista määrityksesi.",
"errorTitle": "Tapahtui virhe",
"errorSubtitle": "Tapahtui virhe yritettäessä suorittaa tämä toiminto. Ole hyvä ja tarkista konsoli saadaksesi lisätietoja.",
"forgotPasswordMessage": "Voit nollata salasanasi vaihtamalla ympäristömuuttujan `USERS`.",
"fieldRequired": "Tämä kenttä on pakollinen",
"invalidInput": "Virheellinen syöte",
"domainWarningTitle": "Virheellinen verkkotunnus",
"domainWarningSubtitle": "Tämä instanssi on määritelty käyttämään osoitetta <code>{{appUrl}}</code>, mutta nykyinen osoite on <code>{{currentUrl}}</code>. Jos jatkat, saatat törmätä ongelmiin autentikoinnissa.",
"ignoreTitle": "Jätä huomiotta",
"goToCorrectDomainTitle": "Siirry oikeaan verkkotunnukseen"
"loginTitle": "Welcome back, login with",
"loginTitleSimple": "Welcome back, please login",
"loginDivider": "Or",
"loginUsername": "Username",
"loginPassword": "Password",
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times. Please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "An error occurred",
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueTitle": "Continue",
"continueSubtitle": "Click the button to continue to your app.",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out",
"logoutSuccessSubtitle": "You have been logged out",
"logoutTitle": "Logout",
"logoutUsernameSubtitle": "You are currently logged in as <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.",
"notFoundTitle": "Page not found",
"notFoundSubtitle": "The page you are looking for does not exist.",
"notFoundButton": "Go home",
"totpFailTitle": "Failed to verify code",
"totpFailSubtitle": "Please check your code and try again",
"totpSuccessTitle": "Verified",
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"totpSubtitle": "Please enter the code from your authenticator app.",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again",
"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?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
}

View File

@@ -1,37 +1,34 @@
{
"loginTitle": "Bienvenue, connectez-vous avec",
"loginTitleSimple": "De retour parmi nous, veuillez vous connecter",
"loginDivider": "Ou",
"loginTitleSimple": "Welcome back, please login",
"loginDivider": "Or",
"loginUsername": "Nom d'utilisateur",
"loginPassword": "Mot de passe",
"loginSubmit": "Se connecter",
"loginFailTitle": "Échec de la connexion",
"loginFailSubtitle": "Veuillez vérifier votre nom d'utilisateur et votre mot de passe",
"loginFailRateLimit": "Vous avez échoué trop de fois à vous connecter. Veuillez réessayer ultérieurement",
"loginFailRateLimit": "You failed to login too many times. Please try again later",
"loginSuccessTitle": "Connecté",
"loginSuccessSubtitle": "Bienvenue!",
"loginSuccessSubtitle": "Bienvenue!",
"loginOauthFailTitle": "Une erreur s'est produite",
"loginOauthFailSubtitle": "Impossible d'obtenir l'URL OAuth",
"loginOauthSuccessTitle": "Redirection",
"loginOauthSuccessSubtitle": "Redirection vers votre fournisseur OAuth",
"loginOauthAutoRedirectTitle": "Redirection automatique OAuth",
"loginOauthAutoRedirectSubtitle": "Vous allez être automatiquement redirigé vers votre fournisseur OAuth pour vous authentifier.",
"loginOauthAutoRedirectButton": "Rediriger",
"continueTitle": "Continuer",
"continueRedirectingTitle": "Redirection...",
"continueRedirectingSubtitle": "Vous devriez être redirigé vers l'application bientôt",
"continueRedirectManually": "Redirection manuelle",
"continueInvalidRedirectTitle": "Redirection invalide",
"continueInvalidRedirectSubtitle": "L'URL de redirection est invalide",
"continueInsecureRedirectTitle": "Redirection non sécurisée",
"continueInsecureRedirectSubtitle": "Vous tentez de rediriger de <code>https</code> vers <code>http</code>, ce qui n'est pas sécurisé. Êtes-vous sûr de vouloir continuer ?",
"continueUntrustedRedirectTitle": "Redirection non sécurisée",
"continueUntrustedRedirectSubtitle": "Vous essayez de rediriger vers un domaine qui ne correspond pas à votre domaine configuré (<code>{{cookieDomain}}</code>). Êtes-vous sûr de vouloir continuer ?",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueTitle": "Continuer",
"continueSubtitle": "Cliquez sur le bouton pour continuer vers votre application.",
"logoutFailTitle": "Échec de la déconnexion",
"logoutFailSubtitle": "Veuillez réessayer",
"logoutSuccessTitle": "Déconnecté",
"logoutSuccessSubtitle": "Vous avez été déconnecté",
"logoutTitle": "Déconnexion",
"logoutUsernameSubtitle": "Vous êtes actuellement connecté en tant que <code>{{username}}</code>. Cliquez sur le bouton ci-dessous pour vous déconnecter.",
"logoutOauthSubtitle": "Vous êtes actuellement connecté en tant que <code>{{username}}</code> via le fournisseur OAuth {{provider}}. Cliquez sur le bouton ci-dessous pour vous déconnecter.",
"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.",
"notFoundTitle": "Page introuvable",
"notFoundSubtitle": "La page recherchée n'existe pas.",
"notFoundButton": "Retour à la page d'accueil",
@@ -40,23 +37,18 @@
"totpSuccessTitle": "Vérifié",
"totpSuccessSubtitle": "Redirection vers votre application",
"totpTitle": "Saisissez votre code TOTP",
"totpSubtitle": "Veuillez saisir le code de votre application d'authentification.",
"totpSubtitle": "Please enter the code from your authenticator app.",
"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>.",
"unauthorizedLoginSubtitle": "L'utilisateur avec le nom d'utilisateur <code>{{username}}</code> n'est pas autorisé à se connecter.",
"unauthorizedGroupsSubtitle": "L'utilisateur avec le nom d'utilisateur <code>{{username}}</code> n'appartient pas aux groupes requis par la ressource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Votre adresse IP <code>{{ip}}</code> n'est pas autorisée à accéder à la ressource <code>{{resource}}</code>.",
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Réessayer",
"cancelTitle": "Annuler",
"forgotPasswordTitle": "Mot de passe oublié ?",
"failedToFetchProvidersTitle": "Échec du chargement des fournisseurs d'authentification. Veuillez vérifier votre configuration.",
"errorTitle": "Une erreur est survenue",
"errorSubtitle": "Une erreur est survenue lors de l'exécution de cette action. Veuillez consulter la console pour plus d'informations.",
"forgotPasswordMessage": "Vous pouvez réinitialiser votre mot de passe en modifiant la variable d'environnement `USERS`.",
"fieldRequired": "Ce champ est obligatoire",
"invalidInput": "Saisie non valide",
"domainWarningTitle": "Domaine invalide",
"domainWarningSubtitle": "Cette instance est configurée pour être accédée depuis <code>{{appUrl}}</code>, mais <code>{{currentUrl}}</code> est utilisé. Si vous continuez, vous pourriez rencontrer des problèmes d'authentification.",
"ignoreTitle": "Ignorer",
"goToCorrectDomainTitle": "Aller au bon domaine"
"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?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
}

View File

@@ -14,17 +14,14 @@
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Continue",
"continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectManually": "Redirect me manually",
"continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"continueTitle": "Continue",
"continueSubtitle": "Click the button to continue to your app.",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out",
@@ -47,16 +44,11 @@
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again",
"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?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
}

View File

@@ -14,17 +14,14 @@
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Continue",
"continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectManually": "Redirect me manually",
"continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"continueTitle": "Continue",
"continueSubtitle": "Click the button to continue to your app.",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out",
@@ -47,16 +44,11 @@
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again",
"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?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
}

View File

@@ -14,17 +14,14 @@
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Continue",
"continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectManually": "Redirect me manually",
"continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"continueTitle": "Continue",
"continueSubtitle": "Click the button to continue to your app.",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out",
@@ -47,16 +44,11 @@
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again",
"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?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
}

View File

@@ -14,17 +14,14 @@
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Continue",
"continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectManually": "Redirect me manually",
"continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"continueTitle": "Continue",
"continueSubtitle": "Click the button to continue to your app.",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out",
@@ -47,16 +44,11 @@
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again",
"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?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
}

View File

@@ -14,17 +14,14 @@
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Continue",
"continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectManually": "Redirect me manually",
"continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"continueTitle": "Continue",
"continueSubtitle": "Click the button to continue to your app.",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out",
@@ -47,16 +44,11 @@
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again",
"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?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
}

View File

@@ -14,17 +14,14 @@
"loginOauthFailSubtitle": "Fout bij het ophalen van OAuth URL",
"loginOauthSuccessTitle": "Omleiden",
"loginOauthSuccessSubtitle": "Omleiden naar je OAuth provider",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Ga verder",
"continueRedirectingTitle": "Omleiden...",
"continueRedirectingSubtitle": "Je wordt naar de app doorgestuurd",
"continueRedirectManually": "Redirect me manually",
"continueInvalidRedirectTitle": "Ongeldige omleiding",
"continueInvalidRedirectSubtitle": "De omleidings-URL is ongeldig",
"continueInsecureRedirectTitle": "Onveilige doorverwijzing",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"continueTitle": "Ga verder",
"continueSubtitle": "Klik op de knop om door te gaan naar de app.",
"logoutFailTitle": "Afmelden mislukt",
"logoutFailSubtitle": "Probeer het opnieuw",
"logoutSuccessTitle": "Afgemeld",
@@ -47,16 +44,11 @@
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Opnieuw proberen",
"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?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
}

View File

@@ -14,17 +14,14 @@
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Continue",
"continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectManually": "Redirect me manually",
"continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"continueTitle": "Continue",
"continueSubtitle": "Click the button to continue to your app.",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out",
@@ -47,16 +44,11 @@
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again",
"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?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
}

View File

@@ -14,22 +14,19 @@
"loginOauthFailSubtitle": "Nie udało się uzyskać adresu URL OAuth",
"loginOauthSuccessTitle": "Przekierowywanie",
"loginOauthSuccessSubtitle": "Przekierowywanie do Twojego dostawcy OAuth",
"loginOauthAutoRedirectTitle": "Automatyczne przekierowanie OAuth",
"loginOauthAutoRedirectSubtitle": "Nastąpi automatyczne przekierowanie do dostawcy OAuth w celu uwierzytelnienia.",
"loginOauthAutoRedirectButton": "Przekieruj teraz",
"continueTitle": "Kontynuuj",
"continueRedirectingTitle": "Przekierowywanie...",
"continueRedirectingSubtitle": "Wkrótce powinieneś zostać przekierowany do aplikacji",
"continueRedirectManually": "Przekieruj mnie ręcznie",
"continueInvalidRedirectTitle": "Nieprawidłowe przekierowanie",
"continueInvalidRedirectSubtitle": "Adres przekierowania jest nieprawidłowy",
"continueInsecureRedirectTitle": "Niezabezpieczone przekierowanie",
"continueInsecureRedirectSubtitle": "Próbujesz przekierować z <code>https</code> do <code>http</code>, co nie jest bezpieczne. Czy na pewno chcesz kontynuować?",
"continueUntrustedRedirectTitle": "Niezaufane przekierowanie",
"continueUntrustedRedirectSubtitle": "Próbujesz przekierować do domeny, która nie pasuje do skonfigurowanej domeny (<code>{{cookieDomain}}</code>). Czy na pewno chcesz kontynuować?",
"continueTitle": "Kontynuuj",
"continueSubtitle": "Kliknij przycisk, aby przejść do aplikacji.",
"logoutFailTitle": "Nie udało się wylogować",
"logoutFailSubtitle": "Spróbuj ponownie",
"logoutSuccessTitle": "Wylogowano",
"logoutSuccessSubtitle": "Zostałeś wylogowany",
"logoutTitle": "Wyloguj się",
"logoutTitle": "Wylogowanie",
"logoutUsernameSubtitle": "Jesteś obecnie zalogowany jako <code>{{username}}</code>. Kliknij poniższy przycisk, aby się wylogować.",
"logoutOauthSubtitle": "Obecnie jesteś zalogowany jako <code>{{username}}</code> przy użyciu dostawcy {{provider}} OAuth. Kliknij poniższy przycisk, aby się wylogować.",
"notFoundTitle": "Nie znaleziono strony",
@@ -47,16 +44,11 @@
"unauthorizedGroupsSubtitle": "Użytkownik o nazwie <code>{{username}}</code> nie należy do grup wymaganych przez zasób <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Twój adres IP <code>{{ip}}</code> nie ma autoryzacji do dostępu do zasobu <code>{{resource}}</code>.",
"unauthorizedButton": "Spróbuj ponownie",
"untrustedRedirectTitle": "Niezaufane przekierowanie",
"untrustedRedirectSubtitle": "Próbujesz przekierować do domeny, która nie pasuje do Twojej skonfigurowanej domeny (<code>{{domain}}</code>). Czy na pewno chcesz kontynuować?",
"cancelTitle": "Anuluj",
"forgotPasswordTitle": "Nie pamiętasz hasła?",
"failedToFetchProvidersTitle": "Nie udało się załadować dostawców uwierzytelniania. Sprawdź swoją konfigurację.",
"errorTitle": "Wystąpił błąd",
"errorSubtitle": "Wystąpił błąd podczas próby wykonania tej czynności. Sprawdź konsolę, aby uzyskać więcej informacji.",
"forgotPasswordMessage": "Możesz zresetować hasło, zmieniając zmienną środowiskową `USERS`.",
"fieldRequired": "To pole jest wymagane",
"invalidInput": "Nieprawidłowe dane wejściowe",
"domainWarningTitle": "Nieprawidłowa domena",
"domainWarningSubtitle": "Ta instancja jest skonfigurowana do uzyskania dostępu z <code>{{appUrl}}</code>, ale <code>{{currentUrl}}</code> jest w użyciu. Jeśli będziesz kontynuować, mogą wystąpić problemy z uwierzytelnianiem.",
"ignoreTitle": "Zignoruj",
"goToCorrectDomainTitle": "Przejdź do prawidłowej domeny"
"errorSubtitle": "Wystąpił błąd podczas próby wykonania tej czynności. Sprawdź konsolę, aby uzyskać więcej informacji."
}

View File

@@ -1,37 +1,34 @@
{
"loginTitle": "Bem-vindo de volta, acesse com",
"loginTitleSimple": "Bem-vindo de volta, faça o login",
"loginDivider": "Ou",
"loginTitleSimple": "Welcome back, please login",
"loginDivider": "Or",
"loginUsername": "Nome de usuário",
"loginPassword": "Senha",
"loginSubmit": "Entrar",
"loginFailTitle": "Falha ao iniciar sessão",
"loginFailSubtitle": "Por favor, verifique seu usuário e senha",
"loginFailRateLimit": "Você falhou em iniciar sessão muitas vezes, por favor tente novamente mais tarde",
"loginFailRateLimit": "You failed to login too many times. Please try again later",
"loginSuccessTitle": "Sessão Iniciada",
"loginSuccessSubtitle": "Bem-vindo de volta!",
"loginOauthFailTitle": "Ocorreu um erro",
"loginOauthFailTitle": "An error occurred",
"loginOauthFailSubtitle": "Falha ao obter URL de OAuth",
"loginOauthSuccessTitle": "Redirecionando",
"loginOauthSuccessSubtitle": "Redirecionando para seu provedor OAuth",
"loginOauthAutoRedirectTitle": "Redirecionamento automático do OAuth",
"loginOauthAutoRedirectSubtitle": "Você será automaticamente redirecionado para seu provedor OAuth para autenticar.",
"loginOauthAutoRedirectButton": "Redirecionar agora",
"continueTitle": "Continuar",
"continueRedirectingTitle": "Redirecionando...",
"continueRedirectingSubtitle": "Você deve ser redirecionado para o aplicativo em breve",
"continueRedirectManually": "Redirecionar-me manualmente",
"continueInvalidRedirectTitle": "Redirecionamento inválido",
"continueInvalidRedirectSubtitle": "O endereço de redirecionamento é inválido",
"continueInsecureRedirectTitle": "Redirecionamento inseguro",
"continueInsecureRedirectSubtitle": "Você está tentando redirecionar de <code>https</code> para <code>http</code>, você tem certeza que deseja continuar?",
"continueUntrustedRedirectTitle": "Redirecionamento não confiável",
"continueUntrustedRedirectSubtitle": "Você está tentando redirecionar para um domínio que não corresponde ao seu domínio configurado (<code>{{cookieDomain}}</code>). Tem certeza que deseja continuar?",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueTitle": "Continuar",
"continueSubtitle": "Clique no botão para continuar para o seu aplicativo.",
"logoutFailTitle": "Falha ao encerrar sessão",
"logoutFailSubtitle": "Por favor, tente novamente",
"logoutSuccessTitle": "Sessão encerrada",
"logoutSuccessSubtitle": "Você foi desconectado",
"logoutTitle": "Sair",
"logoutUsernameSubtitle": "Você está atualmente logado como <code>{{username}}</code>, clique no botão abaixo para sair.",
"logoutOauthSubtitle": "Você está atualmente logado como <code>{{username}}</code> usando o provedor {{provider}} OAuth, clique no botão abaixo para sair.",
"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.",
"notFoundTitle": "Página não encontrada",
"notFoundSubtitle": "A página que você está procurando não existe.",
"notFoundButton": "Voltar para a tela inicial",
@@ -40,23 +37,18 @@
"totpSuccessTitle": "Verificado",
"totpSuccessSubtitle": "Redirecionando para o seu aplicativo",
"totpTitle": "Insira o seu código TOTP",
"totpSubtitle": "Por favor, insira o código do seu aplicativo de autenticação.",
"totpSubtitle": "Please enter the code from your authenticator app.",
"unauthorizedTitle": "Não autorizado",
"unauthorizedResourceSubtitle": "O usuário com nome de usuário <code>{{username}}</code> não está autorizado a acessar o recurso <code>{{resource}}</code>.",
"unauthorizedLoginSubtitle": "O usuário com o nome <code>{{username}}</code> não está autorizado a acessar.",
"unauthorizedGroupsSubtitle": "O usuário <code>{{username}}</code> não está autorizado a acessar o recurso <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Seu endereço IP <code>{{ip}}</code> não está autorizado a acessar o recurso <code>{{resource}}</code>.",
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Tentar novamente",
"untrustedRedirectTitle": "Redirecionamento não confiável",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
"cancelTitle": "Cancelar",
"forgotPasswordTitle": "Esqueceu sua senha?",
"failedToFetchProvidersTitle": "Falha ao carregar provedores de autenticação. Verifique sua configuração.",
"errorTitle": "Ocorreu um erro",
"errorSubtitle": "Ocorreu um erro ao tentar executar esta ação. Por favor, verifique o console para mais informações.",
"forgotPasswordMessage": "Você pode redefinir sua senha alterando a variável de ambiente `USERS`.",
"fieldRequired": "Este campo é obrigatório",
"invalidInput": "Entrada Inválida",
"domainWarningTitle": "Domínio inválido",
"domainWarningSubtitle": "Esta instância está configurada para ser acessada de <code>{{appUrl}}</code>, mas <code>{{currentUrl}}</code> está sendo usado. Se você continuar, você pode encontrar problemas com a autenticação.",
"ignoreTitle": "Ignorar",
"goToCorrectDomainTitle": "Ir para o domínio correto"
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
}

View File

@@ -14,17 +14,14 @@
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Continue",
"continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectManually": "Redirect me manually",
"continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"continueTitle": "Continue",
"continueSubtitle": "Click the button to continue to your app.",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out",
@@ -47,16 +44,11 @@
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again",
"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?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
}

View File

@@ -14,17 +14,14 @@
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Continue",
"continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectManually": "Redirect me manually",
"continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"continueTitle": "Continue",
"continueSubtitle": "Click the button to continue to your app.",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out",
@@ -47,16 +44,11 @@
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again",
"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?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
}

View File

@@ -1,6 +1,6 @@
{
"loginTitle": "С возвращением, войти с",
"loginTitleSimple": "С возвращением, пожалуйста войдите",
"loginTitleSimple": "Вход",
"loginDivider": "Или",
"loginUsername": "Имя пользователя",
"loginPassword": "Пароль",
@@ -8,27 +8,24 @@
"loginFailTitle": "Вход не удался",
"loginFailSubtitle": "Проверьте имя пользователя и пароль",
"loginFailRateLimit": "Слишком много ошибок входа. Попробуйте позже",
"loginSuccessTitle": "Вход выполнен",
"loginSuccessTitle": "Вы вошли",
"loginSuccessSubtitle": "С возвращением!",
"loginOauthFailTitle": "Произошла ошибка",
"loginOauthFailSubtitle": "Не удалось получить ссылку OAuth",
"loginOauthFailSubtitle": "Не удалось получить OAuth URL",
"loginOauthSuccessTitle": "Перенаправление",
"loginOauthSuccessSubtitle": "Перенаправление к поставщику OAuth",
"loginOauthAutoRedirectTitle": "OAuth автоматическое перенаправление",
"loginOauthAutoRedirectSubtitle": "Вы будете автоматически перенаправлены для авторизации у вашего поставщика OAuth.",
"loginOauthAutoRedirectButton": "Перенаправить сейчас",
"continueTitle": "Продолжить",
"continueRedirectingTitle": "Перенаправление...",
"continueRedirectingSubtitle": "Скоро вы будете перенаправлены в приложение",
"continueRedirectManually": "Перенаправить вручную",
"continueInvalidRedirectTitle": "Неверное перенаправление",
"continueInvalidRedirectSubtitle": "URL перенаправления недействителен",
"continueInsecureRedirectTitle": "Небезопасное перенаправление",
"continueInsecureRedirectSubtitle": "Попытка перенаправления с <code>https</code> на <code>http</code>, уверены, что хотите продолжить?",
"continueUntrustedRedirectTitle": "Недоверенное перенаправление",
"continueUntrustedRedirectSubtitle": "Вы пытаетесь перенаправить на домен, который не соответствует вашему настроенному домену (<code>{{cookieDomain}}</code>). Вы уверены, что хотите продолжить?",
"continueTitle": "Продолжить",
"continueSubtitle": "Нажмите на кнопку, чтобы перейти к приложению.",
"logoutFailTitle": "Не удалось выйти",
"logoutFailSubtitle": "Попробуйте ещё раз",
"logoutSuccessTitle": "Выход",
"logoutSuccessSubtitle": "Вы вышли",
"logoutSuccessSubtitle": "Вы вышли из системы",
"logoutTitle": "Выйти",
"logoutUsernameSubtitle": "Вход выполнен как <code>{{username}}</code>, нажмите на кнопку ниже, чтобы выйти.",
"logoutOauthSubtitle": "Вход выполнен как <code>{{username}}</code> с использованием {{provider}} OAuth, нажмите кнопку ниже, чтобы выйти.",
@@ -40,23 +37,18 @@
"totpSuccessTitle": "Подтверждён",
"totpSuccessSubtitle": "Перенаправление в приложение",
"totpTitle": "Введите код TOTP",
"totpSubtitle": "Пожалуйста, введите код из вашего приложения авторизации.",
"unauthorizedTitle": "Доступ запрещён",
"unauthorizedResourceSubtitle": "Пользователю <code>{{username}}</code> не разрешён доступ к <code>{{resource}}</code>.",
"unauthorizedLoginSubtitle": "Пользователю <code>{{username}}</code> не разрешён вход.",
"unauthorizedGroupsSubtitle": "Пользователь <code>{{username}}</code> не состоит в группах, которым разрешён доступ к <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Вашему IP-адресу <code>{{ip}}</code> не разрешён доступ к ресурсу <code>{{resource}}</code>.",
"totpSubtitle": "Пожалуйста, введите код из вашего приложения — аутентификатора.",
"unauthorizedTitle": "Доступ запрещен",
"unauthorizedResourceSubtitle": "Пользователю <code>{{username}}</code> не разрешен доступ к <code>{{resource}}</code>.",
"unauthorizedLoginSubtitle": "Пользователю <code>{{username}}</code> не разрешен вход.",
"unauthorizedGroupsSubtitle": "Пользователь <code>{{username}}</code> не состоит в группах, которым разрешен доступ к <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Ваш IP адрес <code>{{ip}}</code> не авторизован для доступа к ресурсу <code>{{resource}}</code>.",
"unauthorizedButton": "Повторить",
"untrustedRedirectTitle": "Ненадежное перенаправление",
"untrustedRedirectSubtitle": "Попытка перенаправить на домен, который не соответствует вашему заданному домену (<code>{{domain}}</code>). Уверены, что хотите продолжить?",
"cancelTitle": "Отмена",
"forgotPasswordTitle": "Забыли пароль?",
"failedToFetchProvidersTitle": "Не удалось загрузить поставщика авторизации. Пожалуйста, проверьте конфигурацию.",
"failedToFetchProvidersTitle": "Не удалось загрузить провайдеров аутентификации. Пожалуйста, проверьте конфигурацию.",
"errorTitle": "Произошла ошибка",
"errorSubtitle": "Произошла ошибка при попытке выполнить это действие. Проверьте консоль для дополнительной информации.",
"forgotPasswordMessage": "Вы можете сбросить свой пароль, изменив переменную окружения `USERS`.",
"fieldRequired": "Это поле является обязательным",
"invalidInput": "Недопустимый ввод",
"domainWarningTitle": "Неверный домен",
"domainWarningSubtitle": "Этот экземпляр настроен на доступ к нему из <code>{{appUrl}}</code>, но <code>{{currentUrl}}</code> в настоящее время используется. Если вы продолжите, то могут возникнуть проблемы с авторизацией.",
"ignoreTitle": "Игнорировать",
"goToCorrectDomainTitle": "Перейти к правильному домену"
"errorSubtitle": "Произошла ошибка при попытке выполнить это действие. Проверьте консоль для дополнительной информации."
}

View File

@@ -1,62 +1,54 @@
{
"loginTitle": "Добродошли назад, пријавите се са",
"loginTitleSimple": "Добродошли назад, молим вас пријавите се",
"loginDivider": "Или",
"loginUsername": "Корисничко име",
"loginPassword": "Лозинка",
"loginSubmit": "Пријава",
"loginFailTitle": "Неуспешна пријава",
"loginFailSubtitle": "Молим вас проверите ваше корисничко име и лозинку",
"loginFailRateLimit": "Нисте успели да се пријавите превише пута. Молим вас покушајте касније",
"loginSuccessTitle": "Пријављени",
"loginSuccessSubtitle": "Добродошли назад!",
"loginOauthFailTitle": "Појавила се грешка",
"loginOauthFailSubtitle": "Неуспело преузимање OAuth адресе",
"loginOauthSuccessTitle": "Преусмеравање",
"loginOauthSuccessSubtitle": "Преусмеравање на вашег OAuth провајдера",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Настави",
"continueRedirectingTitle": "Преусмеравање...",
"continueRedirectingSubtitle": "Требали би сте ускоро да будете преусмерени на апликацију",
"continueRedirectManually": "Redirect me manually",
"continueInsecureRedirectTitle": "Небезбедно преусмеравање",
"continueInsecureRedirectSubtitle": "Покушавате да преусмерите са <code>https</code> на <code>http</code> што није безбедно. Да ли желите да наставите?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"logoutFailTitle": "Неуспешно одјављивање",
"logoutFailSubtitle": "Молим вас покушајте поново",
"logoutSuccessTitle": "Одјављени",
"logoutSuccessSubtitle": "Одјављени сте",
"logoutTitle": "Одјава",
"logoutUsernameSubtitle": "Тренутно сте пријављени као <code>{{username}}</code>. Кликните на дугме испод да се одјавите.",
"logoutOauthSubtitle": "Тренутно сте пријављени као <code>{{username}}</code> користећи {{provider}} OAuth провајдера. Кликните на дугме испод да се одјавите.",
"notFoundTitle": "Страница није пронађена",
"notFoundSubtitle": "Страница коју тражите не постоји.",
"notFoundButton": "На почетак",
"totpFailTitle": "Неуспело потврђивање кода",
"totpFailSubtitle": "Молим вас проверите ваш код и покушајте поново",
"totpSuccessTitle": "Потврђен",
"totpSuccessSubtitle": "Преусмеравање на вашу апликацију",
"totpTitle": "Унесите ваш TOTP код",
"totpSubtitle": "Молим вас унесите код из ваше апликације за аутентификацију.",
"unauthorizedTitle": "Неауторизован",
"unauthorizedResourceSubtitle": "Корисник са корисничким именом <code>{{username}}</code> није ауторизован да приступи ресурсу <code>{{resource}}</code>.",
"unauthorizedLoginSubtitle": "Корисник са корисничким именом <code>{{username}}</code> није ауторизован за пријављивање.",
"unauthorizedGroupsSubtitle": "Корисник са корисничким именом <code>{{username}}</code> није у групама које захтева ресурс <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Ваша IP адреса <code>{{ip}}</code> није ауторизована да приступи ресурсу <code>{{resource}}</code>.",
"unauthorizedButton": "Покушајте поново",
"cancelTitle": "Поништи",
"forgotPasswordTitle": "Заборавили сте лозинку?",
"failedToFetchProvidersTitle": "Није успело учитавање провајдера аутентификације. Молим вас проверите ваша подешавања.",
"errorTitle": "Појавила се грешка",
"errorSubtitle": "Појавила се грешка при покушају извршавања ове радње. Молим вас проверите конзолу за додатне информације.",
"forgotPasswordMessage": "Можете поништити вашу лозинку променом `USERS` променљиве окружења.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
"loginTitle": "Welcome back, login with",
"loginTitleSimple": "Welcome back, please login",
"loginDivider": "Or",
"loginUsername": "Username",
"loginPassword": "Password",
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times. Please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "An error occurred",
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueTitle": "Continue",
"continueSubtitle": "Click the button to continue to your app.",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out",
"logoutSuccessSubtitle": "You have been logged out",
"logoutTitle": "Logout",
"logoutUsernameSubtitle": "You are currently logged in as <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.",
"notFoundTitle": "Page not found",
"notFoundSubtitle": "The page you are looking for does not exist.",
"notFoundButton": "Go home",
"totpFailTitle": "Failed to verify code",
"totpFailSubtitle": "Please check your code and try again",
"totpSuccessTitle": "Verified",
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"totpSubtitle": "Please enter the code from your authenticator app.",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again",
"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?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
}

View File

@@ -1,35 +1,32 @@
{
"loginTitle": "Välkommen tillbaka, logga in med",
"loginTitleSimple": "Välkommen tillbaka, logga in",
"loginDivider": "Eller",
"loginUsername": "Användarnamn",
"loginPassword": "Lösenord",
"loginSubmit": "Logga in",
"loginFailTitle": "Kunde inte logga in",
"loginFailSubtitle": "Kontrollera ditt användarnamn och lösenord",
"loginFailRateLimit": "Du misslyckades med att logga in för många gånger. Försök igen senare",
"loginSuccessTitle": "Inloggad",
"loginSuccessSubtitle": "Välkommen tillbaka!",
"loginOauthFailTitle": "Ett fel har uppstått",
"loginOauthFailSubtitle": "Kunde inte hämta OAuth URL",
"loginOauthSuccessTitle": "Omdirigerar",
"loginOauthSuccessSubtitle": "Omdirigera till din OAuth leverantör",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Fortsätt",
"continueRedirectingTitle": "Omdirigerar...",
"continueRedirectingSubtitle": "Du bör omdirigeras till appen snart",
"continueRedirectManually": "Redirect me manually",
"continueInsecureRedirectTitle": "Osäker omdirigering",
"continueInsecureRedirectSubtitle": "Du försöker omdirigera från <code>https</code> till <code>http</code> som inte är säker. Är du säker på att du vill fortsätta?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"logoutFailTitle": "Kunde inte logga ut.",
"logoutFailSubtitle": "Vänligen försök igen",
"logoutSuccessTitle": "Utloggad",
"logoutSuccessSubtitle": "Du har blivit utloggad",
"logoutTitle": "Logga ut",
"loginTitle": "Welcome back, login with",
"loginTitleSimple": "Welcome back, please login",
"loginDivider": "Or",
"loginUsername": "Username",
"loginPassword": "Password",
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times. Please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "An error occurred",
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueTitle": "Continue",
"continueSubtitle": "Click the button to continue to your app.",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out",
"logoutSuccessSubtitle": "You have been logged out",
"logoutTitle": "Logout",
"logoutUsernameSubtitle": "You are currently logged in as <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.",
"notFoundTitle": "Page not found",
@@ -41,22 +38,17 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"totpSubtitle": "Please enter the code from your authenticator app.",
"unauthorizedTitle": "Obehörig",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again",
"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?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
}

View File

@@ -14,17 +14,14 @@
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Yönlendiriliyor",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Devam et",
"continueRedirectingTitle": "Yönlendiriliyor...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectManually": "Redirect me manually",
"continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"continueTitle": "Devam et",
"continueSubtitle": "Click the button to continue to your app.",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Lütfen tekrar deneyin",
"logoutSuccessTitle": ıkış yapıldı",
@@ -47,16 +44,11 @@
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again",
"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",
"forgotPasswordTitle": "Forgot your password?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
}

View File

@@ -1,5 +1,5 @@
{
"loginTitle": "З поверненням, увійдіть через",
"loginTitle": "Welcome back, login with",
"loginTitleSimple": "Welcome back, please login",
"loginDivider": "Or",
"loginUsername": "Username",
@@ -14,17 +14,14 @@
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Continue",
"continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectManually": "Redirect me manually",
"continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"continueTitle": "Continue",
"continueSubtitle": "Click the button to continue to your app.",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out",
@@ -47,16 +44,11 @@
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again",
"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?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
}

View File

@@ -14,17 +14,14 @@
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Continue",
"continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectManually": "Redirect me manually",
"continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"continueTitle": "Continue",
"continueSubtitle": "Click the button to continue to your app.",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out",
@@ -47,16 +44,11 @@
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again",
"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": "Bạn quên mật khẩu?",
"failedToFetchProvidersTitle": "Không tải được nhà cung cấp xác thực. Vui lòng kiểm tra cấu hình của bạn.",
"forgotPasswordTitle": "Forgot your password?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitle": "Đã xảy ra lỗi khi thực hiện thao tác này. Vui lòng kiểm tra bảng điều khiển để biết thêm thông tin.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
}

View File

@@ -14,17 +14,14 @@
"loginOauthFailSubtitle": "获取 OAuth URL 失败",
"loginOauthSuccessTitle": "重定向中",
"loginOauthSuccessSubtitle": "重定向到您的 OAuth 提供商",
"loginOauthAutoRedirectTitle": "OAuth自动重定向",
"loginOauthAutoRedirectSubtitle": "您将被自动重定向到您的 OAuth 提供商进行身份验证。",
"loginOauthAutoRedirectButton": "立即跳转",
"continueTitle": "继续",
"continueRedirectingTitle": "正在重定向……",
"continueRedirectingSubtitle": "您应该很快被重定向到应用",
"continueRedirectManually": "请手动跳转",
"continueInvalidRedirectTitle": "无效的重定向",
"continueInvalidRedirectSubtitle": "重定向URL无效",
"continueInsecureRedirectTitle": "不安全的重定向",
"continueInsecureRedirectSubtitle": "您正在尝试从<code>https</code>重定向到<code>http</code>可能存在风险。您确定要继续吗?",
"continueUntrustedRedirectTitle": "不可信的重定向",
"continueUntrustedRedirectSubtitle": "您尝试跳转的域名与配置的域名(<code>{{cookieDomain}}</code>)不匹配。是否继续?",
"continueTitle": "继续",
"continueSubtitle": "点击按钮以继续您的应用。",
"logoutFailTitle": "注销失败",
"logoutFailSubtitle": "请重试",
"logoutSuccessTitle": "已登出",
@@ -33,7 +30,7 @@
"logoutUsernameSubtitle": "您当前登录用户为<code>{{username}}</code>。点击下方按钮注销。",
"logoutOauthSubtitle": "您当前以<code>{{username}}</code>登录,使用的是{{provider}} OAuth 提供商。点击下方按钮注销。",
"notFoundTitle": "无法找到页面",
"notFoundSubtitle": "您访问的页面不存在。",
"notFoundSubtitle": "您正在查找的页面不存在。",
"notFoundButton": "回到主页",
"totpFailTitle": "无法验证代码",
"totpFailSubtitle": "请检查您的代码并重试",
@@ -45,18 +42,13 @@
"unauthorizedResourceSubtitle": "用户名为<code>{{username}}</code>的用户无权访问资源<code>{{resource}}</code>。",
"unauthorizedLoginSubtitle": "用户名为<code>{{username}}</code>的用户无权登录。",
"unauthorizedGroupsSubtitle": "用户名为<code>{{username}}</code>的用户不在资源<code>{{resource}}</code>所需的组中。",
"unauthorizedIpSubtitle": "用户 <code>{{ip}}</code> 无权访问资源 <code>{{resource}}</code>",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "重试",
"untrustedRedirectTitle": "不可信的重定向",
"untrustedRedirectSubtitle": "您正在尝试重定向到一个与您已配置的域名 (<code>{{domain}}</code>) 不匹配的域名。您确定要继续吗?",
"cancelTitle": "取消",
"forgotPasswordTitle": "忘记密码?",
"failedToFetchProvidersTitle": "加载身份验证提供程序失败,请检查您的配置。",
"errorTitle": "发生了错误",
"errorSubtitle": "执行此操作时发生错误,请检查控制台以获取更多信息。",
"forgotPasswordMessage": "您可以通过更改 `USERS ` 环境变量重置您的密码。",
"fieldRequired": "必添字段",
"invalidInput": "无效的输入",
"domainWarningTitle": "无效域名",
"domainWarningSubtitle": "当前实例配置的访问地址为 <code>{{appUrl}}</code>,但您正在使用 <code>{{currentUrl}}</code>。若继续操作,可能会遇到身份验证问题。",
"ignoreTitle": "忽略",
"goToCorrectDomainTitle": "转到正确的域名"
"errorSubtitle": "执行此操作时发生错误,请检查控制台以获取更多信息。"
}

View File

@@ -1,5 +1,5 @@
{
"loginTitle": "歡迎回來,請使用以下方式登入",
"loginTitle": "歡迎回來,請用以下方式登入",
"loginTitleSimple": "歡迎回來,請登入",
"loginDivider": "或",
"loginUsername": "帳號",
@@ -14,17 +14,14 @@
"loginOauthFailSubtitle": "無法取得 OAuth 網址",
"loginOauthSuccessTitle": "重新導向中",
"loginOauthSuccessSubtitle": "正在將您重新導向至 OAuth 供應商",
"loginOauthAutoRedirectTitle": "OAuth 自動跳轉",
"loginOauthAutoRedirectSubtitle": "自動跳轉到 OAuth 供應商進行身份驗證。",
"loginOauthAutoRedirectButton": "立即重新導向",
"continueTitle": "繼續",
"continueRedirectingTitle": "重新導向中……",
"continueRedirectingTitle": "重新導向中...",
"continueRedirectingSubtitle": "您即將被重新導向至應用程式",
"continueRedirectManually": "手動重新導向",
"continueInvalidRedirectTitle": "無效的重新導向",
"continueInvalidRedirectSubtitle": "重新導向的網址無效",
"continueInsecureRedirectTitle": "不安全的重新導向",
"continueInsecureRedirectSubtitle": "您正嘗試從安全的 <code>https</code> 重新導向至不安全的 <code>http</code>。您確定要繼續嗎?",
"continueUntrustedRedirectTitle": "不受信任的重新導向",
"continueUntrustedRedirectSubtitle": "你嘗試重新導向的域名與設定不符(<code>{{cookieDomain}}</code>)。你確定要繼續嗎?",
"continueTitle": "繼續",
"continueSubtitle": "點擊按鈕以繼續前往您的應用程式。",
"logoutFailTitle": "登出失敗",
"logoutFailSubtitle": "請再試一次",
"logoutSuccessTitle": "登出成功",
@@ -47,16 +44,11 @@
"unauthorizedGroupsSubtitle": "使用者 <code>{{username}}</code> 不在存取資源 <code>{{resource}}</code> 所需的群組中。",
"unauthorizedIpSubtitle": "您的 IP 位址 <code>{{ip}}</code> 未被授權存取資源 <code>{{resource}}</code>。",
"unauthorizedButton": "再試一次",
"untrustedRedirectTitle": "不受信任的重新導向",
"untrustedRedirectSubtitle": "您正嘗試重新導向至的網域與您設定的網域 (<code>{{domain}}</code>) 不符。您確定要繼續嗎?",
"cancelTitle": "取消",
"forgotPasswordTitle": "忘記密碼?",
"failedToFetchProvidersTitle": "載入驗證供應商失敗。請檢查您的設定。",
"errorTitle": "發生錯誤",
"errorSubtitle": "執行此操作時發生錯誤。請檢查主控台以獲取更多資訊。",
"forgotPasswordMessage": "透過修改 `USERS` 環境變數,你可以重設你的密碼。",
"fieldRequired": "此為必填欄位",
"invalidInput": "無效的輸入",
"domainWarningTitle": "無效的網域",
"domainWarningSubtitle": "此服務設定為透過 <code>{{appUrl}}</code> 存取,但目前使用的是 <code>{{currentUrl}}</code>。若繼續操作,可能會遇到驗證問題。",
"ignoreTitle": "忽略",
"goToCorrectDomainTitle": "前往正確域名"
"errorSubtitle": "執行此操作時發生錯誤。請檢查主控台以獲取更多資訊。"
}

View File

@@ -9,7 +9,7 @@ export const isValidUrl = (url: string) => {
try {
new URL(url);
return true;
} catch {
} catch (e) {
return false;
}
};

View File

@@ -16,7 +16,6 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { AppContextProvider } from "./context/app-context.tsx";
import { UserContextProvider } from "./context/user-context.tsx";
import { Toaster } from "@/components/ui/sonner";
import { ThemeProvider } from "./components/providers/theme-provider.tsx";
const queryClient = new QueryClient();
@@ -25,27 +24,25 @@ createRoot(document.getElementById("root")!).render(
<QueryClientProvider client={queryClient}>
<AppContextProvider>
<UserContextProvider>
<ThemeProvider defaultTheme="system" storageKey="tinyauth-theme">
<BrowserRouter>
<Routes>
<Route element={<Layout />} errorElement={<ErrorPage />}>
<Route path="/" element={<App />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/logout" element={<LogoutPage />} />
<Route path="/continue" element={<ContinuePage />} />
<Route path="/totp" element={<TotpPage />} />
<Route
path="/forgot-password"
element={<ForgotPasswordPage />}
/>
<Route path="/unauthorized" element={<UnauthorizedPage />} />
<Route path="/error" element={<ErrorPage />} />
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
</BrowserRouter>
<Toaster />
</ThemeProvider>
<BrowserRouter>
<Routes>
<Route element={<Layout />} errorElement={<ErrorPage />}>
<Route path="/" element={<App />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/logout" element={<LogoutPage />} />
<Route path="/continue" element={<ContinuePage />} />
<Route path="/totp" element={<TotpPage />} />
<Route
path="/forgot-password"
element={<ForgotPasswordPage />}
/>
<Route path="/unauthorized" element={<UnauthorizedPage />} />
<Route path="/error" element={<ErrorPage />} />
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
</BrowserRouter>
<Toaster />
</UserContextProvider>
</AppContextProvider>
</QueryClientProvider>

View File

@@ -11,105 +11,60 @@ import { useUserContext } from "@/context/user-context";
import { isValidUrl } from "@/lib/utils";
import { Trans, useTranslation } from "react-i18next";
import { Navigate, useLocation, useNavigate } from "react-router";
import { useEffect, useState } from "react";
import DOMPurify from "dompurify";
import { useState } from "react";
export const ContinuePage = () => {
const { cookieDomain, disableUiWarnings } = useAppContext();
const { isLoggedIn } = useUserContext();
const { search } = useLocation();
const { t } = useTranslation();
const navigate = useNavigate();
if (!isLoggedIn) {
return <Navigate to="/login" />;
}
const { domain, disableContinue } = useAppContext();
const { search } = useLocation();
const [loading, setLoading] = useState(false);
const [showRedirectButton, setShowRedirectButton] = useState(false);
const searchParams = new URLSearchParams(search);
const redirectUri = searchParams.get("redirect_uri");
const redirectURI = searchParams.get("redirect_uri");
const isValidRedirectUri =
redirectUri !== null ? isValidUrl(redirectUri) : false;
const redirectUriObj = isValidRedirectUri
? new URL(redirectUri as string)
: null;
const isTrustedRedirectUri =
redirectUriObj !== null
? redirectUriObj.hostname === cookieDomain ||
redirectUriObj.hostname.endsWith(`.${cookieDomain}`)
: false;
const isAllowedRedirectProto =
redirectUriObj !== null
? redirectUriObj.protocol === "https:" ||
redirectUriObj.protocol === "http:"
: false;
const isHttpsDowngrade =
redirectUriObj !== null
? redirectUriObj.protocol === "http:" &&
window.location.protocol === "https:"
: false;
if (!redirectURI) {
return <Navigate to="/logout" />;
}
if (!isValidUrl(DOMPurify.sanitize(redirectURI))) {
return <Navigate to="/logout" />;
}
const handleRedirect = () => {
setLoading(true);
window.location.assign(redirectUriObj!.toString());
};
useEffect(() => {
if (!isLoggedIn) {
return;
}
if (
(!isValidRedirectUri ||
!isAllowedRedirectProto ||
!isTrustedRedirectUri ||
isHttpsDowngrade) &&
!disableUiWarnings
) {
return;
}
const auto = setTimeout(() => {
handleRedirect();
}, 100);
const reveal = setTimeout(() => {
setLoading(false);
setShowRedirectButton(true);
}, 5000);
return () => {
clearTimeout(auto);
clearTimeout(reveal);
};
}, []);
if (!isLoggedIn) {
return (
<Navigate
to={`/login?redirect_uri=${encodeURIComponent(redirectUri || "")}`}
replace
/>
);
window.location.href = DOMPurify.sanitize(redirectURI);
}
if (!isValidRedirectUri || !isAllowedRedirectProto) {
return <Navigate to="/logout" replace />;
if (disableContinue) {
handleRedirect();
}
if (!isTrustedRedirectUri && !disableUiWarnings) {
const { t } = useTranslation();
const navigate = useNavigate();
const url = new URL(redirectURI);
if (!(url.hostname == domain) && !url.hostname.endsWith(`.${domain}`)) {
return (
<Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm">
<Card className="min-w-xs sm:min-w-sm">
<CardHeader>
<CardTitle className="text-3xl">
{t("continueUntrustedRedirectTitle")}
{t("untrustedRedirectTitle")}
</CardTitle>
<CardDescription>
<Trans
i18nKey="continueUntrustedRedirectSubtitle"
i18nKey="untrustedRedirectSubtitle"
t={t}
components={{
code: <code />,
}}
values={{ cookieDomain }}
values={{ domain }}
/>
</CardDescription>
</CardHeader>
@@ -121,11 +76,7 @@ export const ContinuePage = () => {
>
{t("continueTitle")}
</Button>
<Button
onClick={() => navigate("/logout")}
variant="outline"
disabled={loading}
>
<Button onClick={() => navigate("/logout")} variant="outline" disabled={loading}>
{t("cancelTitle")}
</Button>
</CardFooter>
@@ -133,9 +84,9 @@ export const ContinuePage = () => {
);
}
if (isHttpsDowngrade && !disableUiWarnings) {
if (url.protocol === "http:" && window.location.protocol === "https:") {
return (
<Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm">
<Card className="min-w-xs sm:min-w-sm">
<CardHeader>
<CardTitle className="text-3xl">
{t("continueInsecureRedirectTitle")}
@@ -151,14 +102,14 @@ export const ContinuePage = () => {
</CardDescription>
</CardHeader>
<CardFooter className="flex flex-col items-stretch gap-2">
<Button onClick={handleRedirect} loading={loading} variant="warning">
<Button
onClick={handleRedirect}
loading={loading}
variant="warning"
>
{t("continueTitle")}
</Button>
<Button
onClick={() => navigate("/logout")}
variant="outline"
disabled={loading}
>
<Button onClick={() => navigate("/logout")} variant="outline" disabled={loading}>
{t("cancelTitle")}
</Button>
</CardFooter>
@@ -169,18 +120,17 @@ export const ContinuePage = () => {
return (
<Card className="min-w-xs sm:min-w-sm">
<CardHeader>
<CardTitle className="text-3xl">
{t("continueRedirectingTitle")}
</CardTitle>
<CardDescription>{t("continueRedirectingSubtitle")}</CardDescription>
<CardTitle className="text-3xl">{t("continueTitle")}</CardTitle>
<CardDescription>{t("continueSubtitle")}</CardDescription>
</CardHeader>
{showRedirectButton && (
<CardFooter className="flex flex-col items-stretch">
<Button onClick={handleRedirect}>
{t("continueRedirectManually")}
</Button>
</CardFooter>
)}
<CardFooter className="flex flex-col items-stretch">
<Button
onClick={handleRedirect}
loading={loading}
>
{t("continueTitle")}
</Button>
</CardFooter>
</Card>
);
};

View File

@@ -1,59 +1,46 @@
import { LoginForm } from "@/components/auth/login-form";
import { GenericIcon } from "@/components/icons/generic";
import { GithubIcon } from "@/components/icons/github";
import { GoogleIcon } from "@/components/icons/google";
import { MicrosoftIcon } from "@/components/icons/microsoft";
import { OAuthIcon } from "@/components/icons/oauth";
import { PocketIDIcon } from "@/components/icons/pocket-id";
import { TailscaleIcon } from "@/components/icons/tailscale";
import { Button } from "@/components/ui/button";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
} from "@/components/ui/card";
import { OAuthButton } from "@/components/ui/oauth-button";
import { SeperatorWithChildren } from "@/components/ui/separator";
import { useAppContext } from "@/context/app-context";
import { useUserContext } from "@/context/user-context";
import { useIsMounted } from "@/lib/hooks/use-is-mounted";
import { LoginSchema } from "@/schemas/login-schema";
import { useMutation } from "@tanstack/react-query";
import axios, { AxiosError } from "axios";
import { useEffect, useRef, useState } from "react";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Navigate, useLocation } from "react-router";
import { toast } from "sonner";
const iconMap: Record<string, React.ReactNode> = {
google: <GoogleIcon />,
github: <GithubIcon />,
tailscale: <TailscaleIcon />,
microsoft: <MicrosoftIcon />,
pocketid: <PocketIDIcon />,
};
export const LoginPage = () => {
const { isLoggedIn } = useUserContext();
const { providers, title, oauthAutoRedirect } = useAppContext();
if (isLoggedIn) {
return <Navigate to="/logout" />;
}
const { configuredProviders, title, oauthAutoRedirect, genericName } = useAppContext();
const { search } = useLocation();
const { t } = useTranslation();
const [oauthAutoRedirectHandover, setOauthAutoRedirectHandover] =
useState(false);
const [showRedirectButton, setShowRedirectButton] = useState(false);
const redirectTimer = useRef<number | null>(null);
const redirectButtonTimer = useRef<number | null>(null);
const isMounted = useIsMounted();
const searchParams = new URLSearchParams(search);
const redirectUri = searchParams.get("redirect_uri");
const oauthProviders = providers.filter(
(provider) => provider.id !== "username",
);
const userAuthConfigured =
providers.find((provider) => provider.id === "username") !== undefined;
const oauthConfigured =
configuredProviders.filter((provider) => provider !== "username").length >
0;
const userAuthConfigured = configuredProviders.includes("username");
const oauthMutation = useMutation({
mutationFn: (provider: string) =>
@@ -66,12 +53,11 @@ export const LoginPage = () => {
description: t("loginOauthSuccessSubtitle"),
});
redirectTimer.current = window.setTimeout(() => {
window.location.replace(data.data.url);
setTimeout(() => {
window.location.href = data.data.url;
}, 500);
},
onError: () => {
setOauthAutoRedirectHandover(false);
toast.error(t("loginOauthFailTitle"), {
description: t("loginOauthFailSubtitle"),
});
@@ -79,7 +65,7 @@ export const LoginPage = () => {
});
const loginMutation = useMutation({
mutationFn: (values: LoginSchema) => axios.post("/api/user/login", values),
mutationFn: (values: LoginSchema) => axios.post("/api/login", values),
mutationKey: ["login"],
onSuccess: (data) => {
if (data.data.totpPending) {
@@ -93,7 +79,7 @@ export const LoginPage = () => {
description: t("loginSuccessSubtitle"),
});
redirectTimer.current = window.setTimeout(() => {
setTimeout(() => {
window.location.replace(
`/continue?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`,
);
@@ -110,100 +96,63 @@ export const LoginPage = () => {
});
useEffect(() => {
if (
providers.find((provider) => provider.id === oauthAutoRedirect) &&
!isLoggedIn &&
redirectUri
) {
// Not sure of a better way to do this
// eslint-disable-next-line react-hooks/set-state-in-effect
setOauthAutoRedirectHandover(true);
oauthMutation.mutate(oauthAutoRedirect);
redirectButtonTimer.current = window.setTimeout(() => {
setShowRedirectButton(true);
}, 5000);
if (isMounted()) {
if (
oauthConfigured &&
configuredProviders.includes(oauthAutoRedirect) &&
redirectUri
) {
oauthMutation.mutate(oauthAutoRedirect);
}
}
}, []);
useEffect(
() => () => {
if (redirectTimer.current) clearTimeout(redirectTimer.current);
if (redirectButtonTimer.current)
clearTimeout(redirectButtonTimer.current);
},
[],
);
if (isLoggedIn && redirectUri) {
return (
<Navigate
to={`/continue?redirect_uri=${encodeURIComponent(redirectUri)}`}
replace
/>
);
}
if (isLoggedIn) {
return <Navigate to="/logout" replace />;
}
if (oauthAutoRedirectHandover) {
return (
<Card className="min-w-xs sm:min-w-sm">
<CardHeader>
<CardTitle className="text-3xl">
{t("loginOauthAutoRedirectTitle")}
</CardTitle>
<CardDescription>
{t("loginOauthAutoRedirectSubtitle")}
</CardDescription>
</CardHeader>
{showRedirectButton && (
<CardFooter className="flex flex-col items-stretch">
<Button
onClick={() => {
window.location.replace(oauthMutation.data?.data.url);
}}
>
{t("loginOauthAutoRedirectButton")}
</Button>
</CardFooter>
)}
</Card>
);
}
return (
<Card className="min-w-xs sm:min-w-sm">
<CardHeader>
<CardTitle className="text-center text-3xl">{title}</CardTitle>
{providers.length > 0 && (
{configuredProviders.length > 0 && (
<CardDescription className="text-center">
{oauthProviders.length !== 0
? t("loginTitle")
: t("loginTitleSimple")}
{oauthConfigured ? t("loginTitle") : t("loginTitleSimple")}
</CardDescription>
)}
</CardHeader>
<CardContent className="flex flex-col gap-4">
{oauthProviders.length !== 0 && (
{oauthConfigured && (
<div className="flex flex-col gap-2 items-center justify-center">
{oauthProviders.map((provider) => (
{configuredProviders.includes("google") && (
<OAuthButton
key={provider.id}
title={provider.name}
icon={iconMap[provider.id] ?? <OAuthIcon />}
title="Google"
icon={<GoogleIcon />}
className="w-full"
onClick={() => oauthMutation.mutate(provider.id)}
loading={
oauthMutation.isPending &&
oauthMutation.variables === provider.id
}
onClick={() => oauthMutation.mutate("google")}
loading={oauthMutation.isPending && oauthMutation.variables === "google"}
disabled={oauthMutation.isPending || loginMutation.isPending}
/>
))}
)}
{configuredProviders.includes("github") && (
<OAuthButton
title="Github"
icon={<GithubIcon />}
className="w-full"
onClick={() => oauthMutation.mutate("github")}
loading={oauthMutation.isPending && oauthMutation.variables === "github"}
disabled={oauthMutation.isPending || loginMutation.isPending}
/>
)}
{configuredProviders.includes("generic") && (
<OAuthButton
title={genericName}
icon={<GenericIcon />}
className="w-full"
onClick={() => oauthMutation.mutate("generic")}
loading={oauthMutation.isPending && oauthMutation.variables === "generic"}
disabled={oauthMutation.isPending || loginMutation.isPending}
/>
)}
</div>
)}
{userAuthConfigured && oauthProviders.length !== 0 && (
{userAuthConfigured && oauthConfigured && (
<SeperatorWithChildren>{t("loginDivider")}</SeperatorWithChildren>
)}
{userAuthConfigured && (
@@ -212,7 +161,7 @@ export const LoginPage = () => {
loading={loginMutation.isPending || oauthMutation.isPending}
/>
)}
{providers.length == 0 && (
{configuredProviders.length == 0 && (
<p className="text-center text-red-600 max-w-sm">
{t("failedToFetchProvidersTitle")}
</p>

View File

@@ -6,30 +6,35 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { useAppContext } from "@/context/app-context";
import { useUserContext } from "@/context/user-context";
import { capitalize } from "@/lib/utils";
import { useMutation } from "@tanstack/react-query";
import axios from "axios";
import { useEffect, useRef } from "react";
import { Trans, useTranslation } from "react-i18next";
import { Navigate } from "react-router";
import { toast } from "sonner";
export const LogoutPage = () => {
const { provider, username, isLoggedIn, email, oauthName } = useUserContext();
const { provider, username, isLoggedIn, email } = useUserContext();
if (!isLoggedIn) {
return <Navigate to="/login" />;
}
const { genericName } = useAppContext();
const { t } = useTranslation();
const redirectTimer = useRef<number | null>(null);
const logoutMutation = useMutation({
mutationFn: () => axios.post("/api/user/logout"),
mutationFn: () => axios.post("/api/logout"),
mutationKey: ["logout"],
onSuccess: () => {
toast.success(t("logoutSuccessTitle"), {
description: t("logoutSuccessSubtitle"),
});
redirectTimer.current = window.setTimeout(() => {
window.location.assign("/login");
setTimeout(async () => {
window.location.replace("/login");
}, 500);
},
onError: () => {
@@ -39,17 +44,6 @@ export const LogoutPage = () => {
},
});
useEffect(
() => () => {
if (redirectTimer.current) clearTimeout(redirectTimer.current);
},
[],
);
if (!isLoggedIn) {
return <Navigate to="/login" replace />;
}
return (
<Card className="min-w-xs sm:min-w-sm">
<CardHeader>
@@ -64,7 +58,8 @@ export const LogoutPage = () => {
}}
values={{
username: email,
provider: oauthName,
provider:
provider === "generic" ? genericName : capitalize(provider),
}}
/>
) : (

View File

@@ -12,31 +12,34 @@ import { useUserContext } from "@/context/user-context";
import { TotpSchema } from "@/schemas/totp-schema";
import { useMutation } from "@tanstack/react-query";
import axios from "axios";
import { useEffect, useId, useRef } from "react";
import { useId } from "react";
import { useTranslation } from "react-i18next";
import { Navigate, useLocation } from "react-router";
import { toast } from "sonner";
export const TotpPage = () => {
const { totpPending } = useUserContext();
if (!totpPending) {
return <Navigate to="/" />;
}
const { t } = useTranslation();
const { search } = useLocation();
const formId = useId();
const redirectTimer = useRef<number | null>(null);
const searchParams = new URLSearchParams(search);
const redirectUri = searchParams.get("redirect_uri");
const totpMutation = useMutation({
mutationFn: (values: TotpSchema) => axios.post("/api/user/totp", values),
mutationFn: (values: TotpSchema) => axios.post("/api/totp", values),
mutationKey: ["totp"],
onSuccess: () => {
toast.success(t("totpSuccessTitle"), {
description: t("totpSuccessSubtitle"),
});
redirectTimer.current = window.setTimeout(() => {
setTimeout(() => {
window.location.replace(
`/continue?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`,
);
@@ -49,17 +52,6 @@ export const TotpPage = () => {
},
});
useEffect(
() => () => {
if (redirectTimer.current) clearTimeout(redirectTimer.current);
},
[],
);
if (!totpPending) {
return <Navigate to="/" replace />;
}
return (
<Card className="min-w-xs sm:min-w-sm">
<CardHeader>

View File

@@ -12,10 +12,6 @@ import { Navigate, useLocation, useNavigate } from "react-router";
export const UnauthorizedPage = () => {
const { search } = useLocation();
const { t } = useTranslation();
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const searchParams = new URLSearchParams(search);
const username = searchParams.get("username");
@@ -23,15 +19,19 @@ export const UnauthorizedPage = () => {
const groupErr = searchParams.get("groupErr");
const ip = searchParams.get("ip");
if (!username && !ip) {
return <Navigate to="/" />;
}
const { t } = useTranslation();
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const handleRedirect = () => {
setLoading(true);
navigate("/login");
};
if (!username && !ip) {
return <Navigate to="/" />;
}
let i18nKey = "unauthorizedLoginSubtitle";
if (resource) {

View File

@@ -1,20 +1,17 @@
import { z } from "zod";
export const providerSchema = z.object({
id: z.string(),
name: z.string(),
oauth: z.boolean(),
});
export const appContextSchema = z.object({
providers: z.array(providerSchema),
configuredProviders: z.array(z.string()),
disableContinue: z.boolean(),
title: z.string(),
appUrl: z.string(),
cookieDomain: z.string(),
genericName: z.string(),
domain: z.string(),
forgotPasswordMessage: z.string(),
oauthAutoRedirect: z.enum(["none", "github", "google", "generic"]),
backgroundImage: z.string(),
oauthAutoRedirect: z.string(),
disableUiWarnings: z.boolean(),
version: z.string(),
buildTimestamp: z.string(),
commitHash: z.string(),
});
export type AppContextSchema = z.infer<typeof appContextSchema>;

View File

@@ -8,7 +8,6 @@ export const userContextSchema = z.object({
provider: z.string(),
oauth: z.boolean(),
totpPending: z.boolean(),
oauthName: z.string(),
});
export type UserContextSchema = z.infer<typeof userContextSchema>;

View File

@@ -19,11 +19,6 @@ export default defineConfig({
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
"/resources": {
target: "http://tinyauth-backend:3000/resources",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/resources/, ""),
},
},
allowedHosts: true,
},

120
go.mod
View File

@@ -1,28 +1,18 @@
module tinyauth
go 1.24.0
toolchain go1.24.3
go 1.23.2
require (
github.com/cenkalti/backoff/v5 v5.0.3
github.com/gin-gonic/gin v1.11.0
github.com/glebarez/sqlite v1.11.0
github.com/go-playground/validator/v10 v10.28.0
github.com/golang-migrate/migrate/v4 v4.19.1
github.com/gin-gonic/gin v1.10.1
github.com/go-playground/validator/v10 v10.27.0
github.com/google/go-querystring v1.1.0
github.com/google/uuid v1.6.0
github.com/mdp/qrterminal/v3 v3.2.1
github.com/rs/zerolog v1.34.0
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
github.com/stoewer/go-strcase v1.3.1
github.com/spf13/cobra v1.9.1
github.com/spf13/viper v1.20.1
github.com/traefik/paerser v0.2.2
github.com/weppos/publicsuffix-go v0.50.1
golang.org/x/crypto v0.46.0
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
gorm.io/gorm v1.31.1
gotest.tools/v3 v3.5.2
golang.org/x/crypto v0.40.0
)
require (
@@ -32,72 +22,61 @@ require (
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-sqlite3 v1.14.32 // indirect
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // 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/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect
go.uber.org/mock v0.5.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/term v0.38.0 // indirect
golang.org/x/tools v0.39.0 // indirect
modernc.org/libc v1.66.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.38.2 // indirect
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
golang.org/x/term v0.33.0 // indirect
gotest.tools/v3 v3.5.2 // indirect
rsc.io/qr v0.2.0 // indirect
)
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/boombuler/barcode v1.0.2 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/bytedance/sonic v1.12.7 // indirect
github.com/bytedance/sonic/loader v0.2.3 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
github.com/charmbracelet/bubbletea v1.3.6 // indirect
github.com/charmbracelet/huh v0.8.0
github.com/charmbracelet/bubbles v0.21.0 // indirect
github.com/charmbracelet/bubbletea v1.3.4 // indirect
github.com/charmbracelet/huh v0.7.0
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.9.3 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v28.5.2+incompatible
github.com/docker/docker v28.3.2+incompatible
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-ldap/ldap/v3 v3.4.12
github.com/go-logr/logr v1.4.3 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect
github.com/go-ldap/ldap/v3 v3.4.11
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-json v0.10.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.4.0
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/magiconair/properties v1.8.10
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
@@ -111,27 +90,30 @@ require (
github.com/muesli/termenv v0.16.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pquerna/otp v1.5.0
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.34.0
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
go.opentelemetry.io/otel v1.34.0 // indirect
go.opentelemetry.io/otel/metric v1.34.0 // indirect
go.opentelemetry.io/otel/trace v1.34.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.13.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
google.golang.org/protobuf v1.36.3 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

336
go.sum
View File

@@ -4,41 +4,40 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
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.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q=
github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY=
github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
github.com/charmbracelet/huh v0.7.0 h1:W8S1uyGETgj9Tuda3/JdVkc3x7DBLZYPZc4c+/rnRdc=
github.com/charmbracelet/huh v0.7.0/go.mod h1:UGC3DZHlgOKHvHC07a5vHag41zzhpPFj34U92sOmyuk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
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=
@@ -55,8 +54,9 @@ github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
@@ -68,13 +68,12 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
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.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.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/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.3.2+incompatible h1:wn66NJ6pWB1vBZIilP8G3qQPqHy5XymfYn5vsqeA5oA=
github.com/docker/docker v28.3.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -87,25 +86,21 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@@ -114,27 +109,29 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
@@ -153,24 +150,25 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
@@ -182,8 +180,6 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
@@ -209,27 +205,19 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/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.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.1 h1:4ZAWm0AhCb6+hE+l5Q1NAL0iRn/ZrMwqHRGQiFwj2eg=
github.com/quic-go/quic-go v0.54.1/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -239,143 +227,139 @@ github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs=
github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/traefik/paerser v0.2.2 h1:cpzW/ZrQrBh3mdwD/jnp6aXASiUFKOVr6ldP+keJTcQ=
github.com/traefik/paerser v0.2.2/go.mod h1:7BBDd4FANoVgaTZG+yh26jI6CA2nds7D/4VTEdIsh24=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/weppos/publicsuffix-go v0.50.1 h1:elrBHeSkS/eIb169+DnLrknqmdP4AjT0Q0tEdytz1Og=
github.com/weppos/publicsuffix-go v0.50.1/go.mod h1:znn0JVXjcR5hpUl9pbEogwH6I710rA1AX0QQPT0bf+k=
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/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c h1:AtEkQdl5b6zsybXcbz00j1LwNodDuH6hVifIaNqk7NQ=
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
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/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA=
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=

View File

@@ -4,12 +4,7 @@ import (
"embed"
)
// Frontend
// UI assets
//
//go:embed dist
var FrontendAssets embed.FS
// Migrations
//
//go:embed migrations/*.sql
var Migrations embed.FS
var Assets embed.FS

View File

@@ -1 +0,0 @@
DROP TABLE IF EXISTS "sessions";

View File

@@ -1,10 +0,0 @@
CREATE TABLE IF NOT EXISTS "sessions" (
"uuid" TEXT NOT NULL PRIMARY KEY UNIQUE,
"username" TEXT NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"totp_pending" BOOLEAN NOT NULL,
"oauth_groups" TEXT NULL,
"expiry" INTEGER NOT NULL
);

View File

@@ -1 +0,0 @@
ALTER TABLE "sessions" DROP COLUMN "oauth_name";

View File

@@ -1,10 +0,0 @@
ALTER TABLE "sessions" ADD COLUMN "oauth_name" TEXT;
UPDATE "sessions"
SET "oauth_name" = CASE
WHEN LOWER("provider") = 'github' THEN 'GitHub'
WHEN LOWER("provider") = 'google' THEN 'Google'
ELSE UPPER(SUBSTR("provider", 1, 1)) || SUBSTR("provider", 2)
END
WHERE "oauth_name" IS NULL AND "provider" IS NOT NULL;

510
internal/auth/auth.go Normal file
View File

@@ -0,0 +1,510 @@
package auth
import (
"fmt"
"regexp"
"strings"
"sync"
"time"
"tinyauth/internal/docker"
"tinyauth/internal/ldap"
"tinyauth/internal/types"
"tinyauth/internal/utils"
"github.com/gin-gonic/gin"
"github.com/gorilla/sessions"
"github.com/rs/zerolog/log"
"golang.org/x/crypto/bcrypt"
)
type Auth struct {
Config types.AuthConfig
Docker *docker.Docker
LoginAttempts map[string]*types.LoginAttempt
LoginMutex sync.RWMutex
Store *sessions.CookieStore
LDAP *ldap.LDAP
}
func NewAuth(config types.AuthConfig, docker *docker.Docker, ldap *ldap.LDAP) *Auth {
// Create cookie store
store := sessions.NewCookieStore([]byte(config.HMACSecret), []byte(config.EncryptionSecret))
// Configure cookie store
store.Options = &sessions.Options{
Path: "/",
MaxAge: config.SessionExpiry,
Secure: config.CookieSecure,
HttpOnly: true,
Domain: fmt.Sprintf(".%s", config.Domain),
}
return &Auth{
Config: config,
Docker: docker,
LoginAttempts: make(map[string]*types.LoginAttempt),
Store: store,
LDAP: ldap,
}
}
func (auth *Auth) GetSession(c *gin.Context) (*sessions.Session, error) {
// Get session
session, err := auth.Store.Get(c.Request, auth.Config.SessionCookieName)
if err != nil {
log.Warn().Err(err).Msg("Invalid session, clearing cookie and retrying")
// Delete the session cookie if there is an error
c.SetCookie(auth.Config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.Config.Domain), auth.Config.CookieSecure, true)
// Try to get the session again
session, err = auth.Store.Get(c.Request, auth.Config.SessionCookieName)
if err != nil {
// If we still can't get the session, log the error and return nil
log.Error().Err(err).Msg("Failed to get session")
return nil, err
}
}
return session, nil
}
func (auth *Auth) SearchUser(username string) types.UserSearch {
// Loop through users and return the user if the username matches
log.Debug().Str("username", username).Msg("Searching for user")
if auth.GetLocalUser(username).Username != "" {
log.Debug().Str("username", username).Msg("Found local user")
// If user found, return a user with the username and type "local"
return types.UserSearch{
Username: username,
Type: "local",
}
}
// If no user found, check LDAP
if auth.LDAP != nil {
log.Debug().Str("username", username).Msg("Checking LDAP for user")
userDN, err := auth.LDAP.Search(username)
if err != nil {
log.Warn().Err(err).Str("username", username).Msg("Failed to find user in LDAP")
return types.UserSearch{}
}
// If user found in LDAP, return a user with the DN as username
return types.UserSearch{
Username: userDN,
Type: "ldap",
}
}
return types.UserSearch{}
}
func (auth *Auth) VerifyUser(search types.UserSearch, password string) bool {
// Authenticate the user based on the type
switch search.Type {
case "local":
// Get local user
user := auth.GetLocalUser(search.Username)
// Check if password is correct
return auth.CheckPassword(user, password)
case "ldap":
// If LDAP is configured, bind to the LDAP server with the user DN and password
if auth.LDAP != nil {
log.Debug().Str("username", search.Username).Msg("Binding to LDAP for user authentication")
// Bind to the LDAP server
err := auth.LDAP.Bind(search.Username, password)
if err != nil {
log.Warn().Err(err).Str("username", search.Username).Msg("Failed to bind to LDAP")
return false
}
// If bind is successful, rebind with the LDAP bind user
err = auth.LDAP.Bind(auth.LDAP.Config.BindDN, auth.LDAP.Config.BindPassword)
if err != nil {
log.Error().Err(err).Msg("Failed to rebind with service account after user authentication")
// Consider closing the connection or creating a new one
return false
}
log.Debug().Str("username", search.Username).Msg("LDAP authentication successful")
// Return true if the bind was successful
return true
}
default:
log.Warn().Str("type", search.Type).Msg("Unknown user type for authentication")
return false
}
// If no user found or authentication failed, return false
log.Warn().Str("username", search.Username).Msg("User authentication failed")
return false
}
func (auth *Auth) GetLocalUser(username string) types.User {
// Loop through users and return the user if the username matches
log.Debug().Str("username", username).Msg("Searching for local user")
for _, user := range auth.Config.Users {
if user.Username == username {
return user
}
}
// If no user found, return an empty user
log.Warn().Str("username", username).Msg("Local user not found")
return types.User{}
}
func (auth *Auth) CheckPassword(user types.User, password string) bool {
// Compare the hashed password with the password provided
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(email string) bool {
return utils.CheckFilter(auth.Config.OauthWhitelist, email)
}
func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) error {
log.Debug().Msg("Creating session cookie")
// Get session
session, err := auth.GetSession(c)
if err != nil {
log.Error().Err(err).Msg("Failed to get session")
return err
}
log.Debug().Msg("Setting session cookie")
// Calculate expiry
var sessionExpiry int
if data.TotpPending {
sessionExpiry = 3600
} else {
sessionExpiry = auth.Config.SessionExpiry
}
// Set data
session.Values["username"] = data.Username
session.Values["name"] = data.Name
session.Values["email"] = data.Email
session.Values["provider"] = data.Provider
session.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix()
session.Values["totpPending"] = data.TotpPending
session.Values["oauthGroups"] = data.OAuthGroups
// Save session
err = session.Save(c.Request, c.Writer)
if err != nil {
log.Error().Err(err).Msg("Failed to save session")
return err
}
// Return nil
return nil
}
func (auth *Auth) DeleteSessionCookie(c *gin.Context) error {
log.Debug().Msg("Deleting session cookie")
// Get session
session, err := auth.GetSession(c)
if err != nil {
log.Error().Err(err).Msg("Failed to get session")
return err
}
// Delete all values in the session
for key := range session.Values {
delete(session.Values, key)
}
// Save session
err = session.Save(c.Request, c.Writer)
if err != nil {
log.Error().Err(err).Msg("Failed to save session")
return err
}
// Return nil
return nil
}
func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) {
log.Debug().Msg("Getting session cookie")
// Get session
session, err := auth.GetSession(c)
if err != nil {
log.Error().Err(err).Msg("Failed to get session")
return types.SessionCookie{}, err
}
log.Debug().Msg("Got session")
// Get data from session
username, usernameOk := session.Values["username"].(string)
email, emailOk := session.Values["email"].(string)
name, nameOk := session.Values["name"].(string)
provider, providerOK := session.Values["provider"].(string)
expiry, expiryOk := session.Values["expiry"].(int64)
totpPending, totpPendingOk := session.Values["totpPending"].(bool)
oauthGroups, oauthGroupsOk := session.Values["oauthGroups"].(string)
if !usernameOk || !providerOK || !expiryOk || !totpPendingOk || !emailOk || !nameOk || !oauthGroupsOk {
log.Warn().Msg("Session cookie is invalid")
// If any data is missing, delete the session cookie
auth.DeleteSessionCookie(c)
// Return empty cookie
return types.SessionCookie{}, nil
}
// Check if the cookie has expired
if time.Now().Unix() > expiry {
log.Warn().Msg("Session cookie expired")
// If it has, delete it
auth.DeleteSessionCookie(c)
// Return empty cookie
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")
// Return the cookie
return types.SessionCookie{
Username: username,
Name: name,
Email: email,
Provider: provider,
TotpPending: totpPending,
OAuthGroups: oauthGroups,
}, nil
}
func (auth *Auth) UserAuthConfigured() bool {
// If there are users, return true
return len(auth.Config.Users) > 0 || auth.LDAP != nil
}
func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext, labels types.Labels) bool {
// Check if oauth is allowed
if context.OAuth {
log.Debug().Msg("Checking OAuth whitelist")
return utils.CheckFilter(labels.OAuth.Whitelist, context.Email)
}
// Check users
log.Debug().Msg("Checking users")
return utils.CheckFilter(labels.Users, context.Username)
}
func (auth *Auth) OAuthGroup(c *gin.Context, context types.UserContext, labels types.Labels) bool {
// Check if groups are required
if labels.OAuth.Groups == "" {
return true
}
// Check if we are using the generic oauth provider
if context.Provider != "generic" {
log.Debug().Msg("Not using generic provider, skipping group check")
return true
}
// 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.CheckFilter(labels.OAuth.Groups, group) {
log.Debug().Str("group", group).Msg("Group is in required groups")
return true
}
}
// No groups matched
log.Debug().Msg("No groups matched")
// Return false
return false
}
func (auth *Auth) AuthEnabled(c *gin.Context, labels types.Labels) (bool, error) {
// Get headers
uri := c.Request.Header.Get("X-Forwarded-Uri")
// Check if the allowed label is empty
if labels.Allowed == "" {
// Auth enabled
return true, nil
}
// Compile regex
regex, err := regexp.Compile(labels.Allowed)
// If there is an error, invalid regex, auth enabled
if err != nil {
log.Warn().Err(err).Msg("Invalid regex")
return true, err
}
// Check if the uri matches the regex
if regex.MatchString(uri) {
// Auth disabled
return false, nil
}
// Auth enabled
return true, nil
}
func (auth *Auth) GetBasicAuth(c *gin.Context) *types.User {
// Get the Authorization header
username, password, ok := c.Request.BasicAuth()
// If not ok, return an empty user
if !ok {
return nil
}
// Return the user
return &types.User{
Username: username,
Password: password,
}
}
func (auth *Auth) CheckIP(labels types.Labels, ip string) bool {
// Check if the IP is in block list
for _, blocked := range labels.IP.Block {
res, err := utils.FilterIP(blocked, ip)
if err != nil {
log.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list")
continue
}
if res {
log.Warn().Str("ip", ip).Str("item", blocked).Msg("IP is in blocked list, denying access")
return false
}
}
// For every IP in the allow list, check if the IP matches
for _, allowed := range labels.IP.Allow {
res, err := utils.FilterIP(allowed, ip)
if err != nil {
log.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list")
continue
}
if res {
log.Debug().Str("ip", ip).Str("item", allowed).Msg("IP is in allowed list, allowing access")
return true
}
}
// If not in allowed range and allowed range is not empty, deny access
if len(labels.IP.Allow) > 0 {
log.Warn().Str("ip", ip).Msg("IP not in allow list, denying access")
return false
}
log.Debug().Str("ip", ip).Msg("IP not in allow or block list, allowing by default")
return true
}
func (auth *Auth) BypassedIP(labels types.Labels, ip string) bool {
// For every IP in the bypass list, check if the IP matches
for _, bypassed := range labels.IP.Bypass {
res, err := utils.FilterIP(bypassed, ip)
if err != nil {
log.Warn().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list")
continue
}
if res {
log.Debug().Str("ip", ip).Str("item", bypassed).Msg("IP is in bypass list, allowing access")
return true
}
}
log.Debug().Str("ip", ip).Msg("IP not in bypass list, continuing with authentication")
return false
}

Some files were not shown because too many files have changed in this diff Show More