mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-07-11 04:21:12 +00:00
Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ff271e7f18 | |||
| 405d91065e | |||
| 82993624ba | |||
| 0812bc3869 | |||
| 6ff54f62fd | |||
| ff968ef08f | |||
| 464769c673 | |||
| b4c7a877ab | |||
| 67d0e9321f | |||
| f4faa422ae | |||
| 3d8288a7eb | |||
| 96d0a2a3ab | |||
| 0bd2821a9b | |||
| 364175adc0 | |||
| 73cc4808bc | |||
| 7abd9fe310 | |||
| 6ab9c0a0c5 | |||
| 4aa05aeb79 | |||
| 440a3a3ef5 | |||
| a3c4d6ac83 | |||
| c8b31c54a0 | |||
| 04b93fa107 | |||
| a6c716c4e2 | |||
| ffafb5bff5 | |||
| bb867ea5f4 | |||
| fdd516edf1 | |||
| 1b14b90ede | |||
| 6ba55b3d9c | |||
| 09ec40cb76 | |||
| 08af4557fd | |||
| 45a88ea041 | |||
| 89ffdf7e22 | |||
| c692dfe422 | |||
| ac819cc868 | |||
| 69f4206f65 | |||
| 2572376686 | |||
| ea1baaa9ac | |||
| 72d39a23a0 | |||
| efe373084f | |||
| 7f18b45e21 | |||
| 6ccc894570 | |||
| 53af1b99c0 | |||
| 654b5cc436 | |||
| f7d7f1c4f0 | |||
| e7d26f497d | |||
| a9face749d | |||
| 905f67292c | |||
| 6ed5c2d0a0 | |||
| 9dd4515464 | |||
| 40bcc7d9d8 | |||
| 556096cdb8 | |||
| c825d81b2d | |||
| f404c2ef16 | |||
| a0e74cd5f2 | |||
| 49105ce5ff | |||
| 57c573502d | |||
| 426eac2d0b | |||
| da17be400e | |||
| 514fcb8fcc | |||
| 831180c7fa | |||
| e0ab7c75bc | |||
| 66546439fa | |||
| df742abb8d | |||
| 57e1f963df | |||
| d7c255948c | |||
| dac844595d | |||
| 940ba6dff7 | |||
| faee58ca8e | |||
| e9b8ca3cf8 | |||
| f2c4e7932d | |||
| 4538922caf | |||
| 672db84200 | |||
| 359000f731 | |||
| 0a3e7bf265 | |||
| c3461131f5 | |||
| 3f584ca741 | |||
| 36d0ffc2b5 | |||
| 37b79735f0 | |||
| 09540fbe6e | |||
| 8e60a2e28e | |||
| 9619024c37 | |||
| 1c305bacca | |||
| e532cde2b6 | |||
| 2737a25227 | |||
| 7aa25210f5 | |||
| 55bef72639 | |||
| ae17bd3b66 |
+72
-1
@@ -7,7 +7,9 @@ TINYAUTH_APPURL=
|
||||
|
||||
# database config
|
||||
|
||||
# The path to the database, including file name.
|
||||
# The database driver to use. Valid values: sqlite, postgres, memory.
|
||||
TINYAUTH_DATABASE_DRIVER="sqlite"
|
||||
# The path to the SQLite database file, or connection URL when driver is postgres.
|
||||
TINYAUTH_DATABASE_PATH="./tinyauth.db"
|
||||
|
||||
# analytics config
|
||||
@@ -37,8 +39,52 @@ TINYAUTH_SERVER_SOCKETPATH=
|
||||
TINYAUTH_AUTH_IP_ALLOW=
|
||||
# List of blocked IPs or CIDR ranges.
|
||||
TINYAUTH_AUTH_IP_BLOCK=
|
||||
# List of IPs or CIDR ranges that bypass authentication entirely.
|
||||
TINYAUTH_AUTH_IP_BYPASS=
|
||||
# Comma-separated list of users (username:hashed_password).
|
||||
TINYAUTH_AUTH_USERS=
|
||||
# Enable subdomains support.
|
||||
TINYAUTH_AUTH_SUBDOMAINSENABLED=true
|
||||
# Full name of the user.
|
||||
TINYAUTH_AUTH_USERATTRIBUTES_name_NAME=
|
||||
# Given (first) name of the user.
|
||||
TINYAUTH_AUTH_USERATTRIBUTES_name_GIVENNAME=
|
||||
# Family (last) name of the user.
|
||||
TINYAUTH_AUTH_USERATTRIBUTES_name_FAMILYNAME=
|
||||
# Middle name of the user.
|
||||
TINYAUTH_AUTH_USERATTRIBUTES_name_MIDDLENAME=
|
||||
# Nickname of the user.
|
||||
TINYAUTH_AUTH_USERATTRIBUTES_name_NICKNAME=
|
||||
# URL of the user's profile page.
|
||||
TINYAUTH_AUTH_USERATTRIBUTES_name_PROFILE=
|
||||
# URL of the user's profile picture.
|
||||
TINYAUTH_AUTH_USERATTRIBUTES_name_PICTURE=
|
||||
# URL of the user's website.
|
||||
TINYAUTH_AUTH_USERATTRIBUTES_name_WEBSITE=
|
||||
# Email address of the user.
|
||||
TINYAUTH_AUTH_USERATTRIBUTES_name_EMAIL=
|
||||
# Gender of the user.
|
||||
TINYAUTH_AUTH_USERATTRIBUTES_name_GENDER=
|
||||
# Birthdate of the user (YYYY-MM-DD).
|
||||
TINYAUTH_AUTH_USERATTRIBUTES_name_BIRTHDATE=
|
||||
# Time zone of the user (e.g. Europe/Athens).
|
||||
TINYAUTH_AUTH_USERATTRIBUTES_name_ZONEINFO=
|
||||
# Locale of the user (e.g. en-US).
|
||||
TINYAUTH_AUTH_USERATTRIBUTES_name_LOCALE=
|
||||
# Phone number of the user.
|
||||
TINYAUTH_AUTH_USERATTRIBUTES_name_PHONENUMBER=
|
||||
# Full mailing address, formatted for display.
|
||||
TINYAUTH_AUTH_USERATTRIBUTES_name_ADDRESS_FORMATTED=
|
||||
# Street address.
|
||||
TINYAUTH_AUTH_USERATTRIBUTES_name_ADDRESS_STREETADDRESS=
|
||||
# City or locality.
|
||||
TINYAUTH_AUTH_USERATTRIBUTES_name_ADDRESS_LOCALITY=
|
||||
# State, province, or region.
|
||||
TINYAUTH_AUTH_USERATTRIBUTES_name_ADDRESS_REGION=
|
||||
# Zip or postal code.
|
||||
TINYAUTH_AUTH_USERATTRIBUTES_name_ADDRESS_POSTALCODE=
|
||||
# Country.
|
||||
TINYAUTH_AUTH_USERATTRIBUTES_name_ADDRESS_COUNTRY=
|
||||
# Path to the users file.
|
||||
TINYAUTH_AUTH_USERSFILE=
|
||||
# Enable secure cookies.
|
||||
@@ -51,8 +97,12 @@ TINYAUTH_AUTH_SESSIONMAXLIFETIME=0
|
||||
TINYAUTH_AUTH_LOGINTIMEOUT=300
|
||||
# Maximum login retries.
|
||||
TINYAUTH_AUTH_LOGINMAXRETRIES=3
|
||||
# Enable lockdown mode after maximum login retries. Lockdown mode limit is calculated automatically.
|
||||
TINYAUTH_AUTH_LOCKDOWNENABLED=true
|
||||
# Comma-separated list of trusted proxy addresses.
|
||||
TINYAUTH_AUTH_TRUSTEDPROXIES=
|
||||
# ACL policy for allow-by-default or deny-by-default, available options are allow and deny, default is allow.
|
||||
TINYAUTH_AUTH_ACLS_POLICY="allow"
|
||||
|
||||
# apps config
|
||||
|
||||
@@ -101,6 +151,10 @@ TINYAUTH_OAUTH_PROVIDERS_name_CLIENTID=
|
||||
TINYAUTH_OAUTH_PROVIDERS_name_CLIENTSECRET=
|
||||
# Path to the file containing the OAuth client secret.
|
||||
TINYAUTH_OAUTH_PROVIDERS_name_CLIENTSECRETFILE=
|
||||
# Comma-separated list of allowed OAuth domains for this provider.
|
||||
TINYAUTH_OAUTH_PROVIDERS_name_WHITELIST=
|
||||
# Path to the OAuth whitelist file for this provider.
|
||||
TINYAUTH_OAUTH_PROVIDERS_name_WHITELISTFILE=
|
||||
# OAuth scopes.
|
||||
TINYAUTH_OAUTH_PROVIDERS_name_SCOPES=
|
||||
# OAuth redirect URL.
|
||||
@@ -152,6 +206,8 @@ TINYAUTH_LDAP_ADDRESS=
|
||||
TINYAUTH_LDAP_BINDDN=
|
||||
# Bind password for LDAP authentication.
|
||||
TINYAUTH_LDAP_BINDPASSWORD=
|
||||
# Path to the Bind password.
|
||||
TINYAUTH_LDAP_BINDPASSWORDFILE=
|
||||
# Base DN for LDAP searches.
|
||||
TINYAUTH_LDAP_BASEDN=
|
||||
# Allow insecure LDAP connections.
|
||||
@@ -165,6 +221,21 @@ TINYAUTH_LDAP_AUTHKEY=
|
||||
# Cache duration for LDAP group membership in seconds.
|
||||
TINYAUTH_LDAP_GROUPCACHETTL=900
|
||||
|
||||
# tailscale config
|
||||
|
||||
# Enable Tailscale integration.
|
||||
TINYAUTH_TAILSCALE_ENABLED=false
|
||||
# Tailscale API token.
|
||||
TINYAUTH_TAILSCALE_APITOKEN=
|
||||
# Path to the file containing the Tailscale API token.
|
||||
TINYAUTH_TAILSCALE_APITOKENFILE=
|
||||
# Tailnet name.
|
||||
TINYAUTH_TAILSCALE_TAILNET=
|
||||
# Cache duration for Tailscale device and user lists in seconds.
|
||||
TINYAUTH_TAILSCALE_CACHEDURATION=300
|
||||
# Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment.
|
||||
TINYAUTH_LABELPROVIDER="auto"
|
||||
|
||||
# log config
|
||||
|
||||
# Log level (trace, debug, info, warn, error).
|
||||
|
||||
@@ -13,32 +13,32 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
package_json_file: ./frontend/package.json
|
||||
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
|
||||
with:
|
||||
go-version: "^1.26.0"
|
||||
go-version: "^1.26.4"
|
||||
|
||||
- name: Go dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Setup sqlc
|
||||
uses: sqlc-dev/setup-sqlc@v4
|
||||
uses: sqlc-dev/setup-sqlc@v5
|
||||
with:
|
||||
sqlc-version: "1.31.1"
|
||||
|
||||
- name: Check codegen is up to date
|
||||
run: |
|
||||
sqlc generate
|
||||
go generate ./internal/repository/...
|
||||
git diff --exit-code -- internal/repository/
|
||||
git status --porcelain -- internal/repository/ | grep -q . && echo "untracked files in internal/repository/" && exit 1 || true
|
||||
go generate ./...
|
||||
git diff --exit-code
|
||||
git status --porcelain | grep -q . && echo "untracked files code gen files" && exit 1 || true
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: ./frontend
|
||||
@@ -62,6 +62,6 @@ jobs:
|
||||
run: go test -coverprofile=coverage.txt -v ./...
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Delete old release
|
||||
run: gh release delete --cleanup-tag --yes nightly || echo release not found
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
REPO: ${{ github.event.repository.name }}
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
|
||||
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3
|
||||
with:
|
||||
prerelease: true
|
||||
tag_name: nightly
|
||||
@@ -35,9 +35,10 @@ jobs:
|
||||
VERSION: ${{ steps.metadata.outputs.VERSION }}
|
||||
COMMIT_HASH: ${{ steps.metadata.outputs.COMMIT_HASH }}
|
||||
BUILD_TIMESTAMP: ${{ steps.metadata.outputs.BUILD_TIMESTAMP }}
|
||||
BUILD_TAGS: ${{ steps.metadata.outputs.BUILD_TAGS }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: nightly
|
||||
|
||||
@@ -47,6 +48,8 @@ jobs:
|
||||
echo "VERSION=nightly" >> "$GITHUB_OUTPUT"
|
||||
echo "COMMIT_HASH=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
echo "BUILD_TIMESTAMP=$(date '+%Y-%m-%dT%H:%M:%S')" >> "$GITHUB_OUTPUT"
|
||||
export GIN_BUILD_TAGS=nomsgpack
|
||||
echo "BUILD_TAGS=$GIN_BUILD_TAGS" >> "$GITHUB_OUTPUT"
|
||||
|
||||
binary-build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -55,19 +58,19 @@ jobs:
|
||||
- generate-metadata
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: nightly
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
package_json_file: ./frontend/package.json
|
||||
|
||||
- name: Install go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
|
||||
with:
|
||||
go-version: "^1.26.0"
|
||||
go-version: "^1.26.4"
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: ./frontend
|
||||
@@ -83,7 +86,11 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
cp -r frontend/dist internal/assets/dist
|
||||
go build -ldflags "-s -w -X github.com/tinyauthapp/tinyauth/internal/model.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 ./cmd/tinyauth
|
||||
go build -tags ${{ needs.generate-metadata.outputs.BUILD_TAGS }} \
|
||||
-ldflags "-X github.com/tinyauthapp/tinyauth/internal/model.Version=${{ needs.generate-metadata.outputs.VERSION }} \
|
||||
-X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} \
|
||||
-X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" \
|
||||
-o tinyauth-amd64 ./cmd/tinyauth
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
|
||||
@@ -100,19 +107,19 @@ jobs:
|
||||
- generate-metadata
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: nightly
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
package_json_file: ./frontend/package.json
|
||||
|
||||
- name: Install go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
|
||||
with:
|
||||
go-version: "^1.26.0"
|
||||
go-version: "^1.26.4"
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: ./frontend
|
||||
@@ -128,7 +135,11 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
cp -r frontend/dist internal/assets/dist
|
||||
go build -ldflags "-s -w -X github.com/tinyauthapp/tinyauth/internal/model.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 ./cmd/tinyauth
|
||||
go build -tags ${{ needs.generate-metadata.outputs.BUILD_TAGS }} \
|
||||
-ldflags "-X github.com/tinyauthapp/tinyauth/internal/model.Version=${{ needs.generate-metadata.outputs.VERSION }} \
|
||||
-X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} \
|
||||
-X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" \
|
||||
-o tinyauth-arm64 ./cmd/tinyauth
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
|
||||
@@ -145,41 +156,42 @@ jobs:
|
||||
- generate-metadata
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: nightly
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
||||
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
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
cache-from: type=gha,scope=buildkit-amd64
|
||||
cache-to: type=gha,mode=max,scope=buildkit-amd64
|
||||
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 }}
|
||||
BUILD_TAGS=${{ needs.generate-metadata.outputs.BUILD_TAGS }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
@@ -190,7 +202,7 @@ jobs:
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: digests-linux-amd64
|
||||
name: digests-alpine-linux-amd64
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
@@ -203,28 +215,28 @@ jobs:
|
||||
- image-build
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: nightly
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
||||
id: build
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
@@ -232,13 +244,14 @@ jobs:
|
||||
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
|
||||
cache-from: type=gha,scope=buildkit-distroless-amd64
|
||||
cache-to: type=gha,mode=max,scope=buildkit-distroless-amd64
|
||||
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 }}
|
||||
BUILD_TAGS=${{ needs.generate-metadata.outputs.BUILD_TAGS }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
@@ -261,41 +274,42 @@ jobs:
|
||||
- generate-metadata
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: nightly
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
||||
id: build
|
||||
with:
|
||||
platforms: linux/arm64
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
cache-from: type=gha,scope=buildkit-arm64
|
||||
cache-to: type=gha,mode=max,scope=buildkit-arm64
|
||||
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 }}
|
||||
BUILD_TAGS=${{ needs.generate-metadata.outputs.BUILD_TAGS }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
@@ -306,7 +320,7 @@ jobs:
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: digests-linux-arm64
|
||||
name: digests-alpine-linux-arm64
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
@@ -319,28 +333,28 @@ jobs:
|
||||
- image-build-arm
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: nightly
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
||||
id: build
|
||||
with:
|
||||
platforms: linux/arm64
|
||||
@@ -348,13 +362,14 @@ jobs:
|
||||
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
|
||||
cache-from: type=gha,scope=buildkit-distroless-arm64
|
||||
cache-to: type=gha,mode=max,scope=buildkit-distroless-arm64
|
||||
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 }}
|
||||
BUILD_TAGS=${{ needs.generate-metadata.outputs.BUILD_TAGS }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
@@ -380,22 +395,22 @@ jobs:
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digests-*
|
||||
pattern: digests-alpine-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
flavor: |
|
||||
@@ -423,18 +438,18 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
flavor: |
|
||||
@@ -461,7 +476,7 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
|
||||
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3
|
||||
with:
|
||||
files: binaries/*
|
||||
tag_name: nightly
|
||||
|
||||
@@ -16,16 +16,21 @@ jobs:
|
||||
VERSION: ${{ steps.metadata.outputs.VERSION }}
|
||||
COMMIT_HASH: ${{ steps.metadata.outputs.COMMIT_HASH }}
|
||||
BUILD_TIMESTAMP: ${{ steps.metadata.outputs.BUILD_TIMESTAMP }}
|
||||
BUILD_TAGS: ${{ steps.metadata.outputs.BUILD_TAGS }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Generate metadata
|
||||
id: metadata
|
||||
env:
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
echo "VERSION=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
|
||||
echo "VERSION=$REF_NAME" >> "$GITHUB_OUTPUT"
|
||||
echo "COMMIT_HASH=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
echo "BUILD_TIMESTAMP=$(date '+%Y-%m-%dT%H:%M:%S')" >> "$GITHUB_OUTPUT"
|
||||
export GIN_BUILD_TAGS=nomsgpack
|
||||
echo "BUILD_TAGS=$GIN_BUILD_TAGS" >> "$GITHUB_OUTPUT"
|
||||
|
||||
binary-build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -33,17 +38,17 @@ jobs:
|
||||
- generate-metadata
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
package_json_file: ./frontend/package.json
|
||||
|
||||
- name: Install go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
|
||||
with:
|
||||
go-version: "^1.26.0"
|
||||
go-version: "^1.26.4"
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: ./frontend
|
||||
@@ -59,7 +64,11 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
cp -r frontend/dist internal/assets/dist
|
||||
go build -ldflags "-s -w -X github.com/tinyauthapp/tinyauth/internal/model.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 ./cmd/tinyauth
|
||||
go build -tags ${{ needs.generate-metadata.outputs.BUILD_TAGS }} \
|
||||
-ldflags "-s -w -X github.com/tinyauthapp/tinyauth/internal/model.Version=${{ needs.generate-metadata.outputs.VERSION }} \
|
||||
-X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} \
|
||||
-X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" \
|
||||
-o tinyauth-amd64 ./cmd/tinyauth
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
|
||||
@@ -75,17 +84,17 @@ jobs:
|
||||
- generate-metadata
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
package_json_file: ./frontend/package.json
|
||||
|
||||
- name: Install go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
|
||||
with:
|
||||
go-version: "^1.26.0"
|
||||
go-version: "^1.26.4"
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: ./frontend
|
||||
@@ -101,7 +110,11 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
cp -r frontend/dist internal/assets/dist
|
||||
go build -ldflags "-s -w -X github.com/tinyauthapp/tinyauth/internal/model.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 ./cmd/tinyauth
|
||||
go build -tags ${{ needs.generate-metadata.outputs.BUILD_TAGS }} \
|
||||
-ldflags "-s -w -X github.com/tinyauthapp/tinyauth/internal/model.Version=${{ needs.generate-metadata.outputs.VERSION }} \
|
||||
-X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} \
|
||||
-X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" \
|
||||
-o tinyauth-arm64 ./cmd/tinyauth
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
|
||||
@@ -117,39 +130,41 @@ jobs:
|
||||
- generate-metadata
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
||||
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
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
cache-from: type=gha,scope=buildkit-amd64
|
||||
cache-to: type=gha,mode=max,scope=buildkit-amd64
|
||||
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 }}
|
||||
LDFLAGS=-s -w
|
||||
BUILD_TAGS=${{ needs.generate-metadata.outputs.BUILD_TAGS }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
@@ -160,7 +175,7 @@ jobs:
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: digests-linux-amd64
|
||||
name: digests-alpine-linux-amd64
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
@@ -172,26 +187,26 @@ jobs:
|
||||
- image-build
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
||||
id: build
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
@@ -199,13 +214,15 @@ jobs:
|
||||
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
|
||||
cache-from: type=gha,scope=buildkit-distroless-amd64
|
||||
cache-to: type=gha,mode=max,scope=buildkit-distroless-amd64
|
||||
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 }}
|
||||
LDFLAGS=-s -w
|
||||
BUILD_TAGS=${{ needs.generate-metadata.outputs.BUILD_TAGS }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
@@ -227,39 +244,41 @@ jobs:
|
||||
- generate-metadata
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
||||
id: build
|
||||
with:
|
||||
platforms: linux/arm64
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
cache-from: type=gha,scope=buildkit-arm64
|
||||
cache-to: type=gha,mode=max,scope=buildkit-arm64
|
||||
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 }}
|
||||
LDFLAGS=-s -w
|
||||
BUILD_TAGS=${{ needs.generate-metadata.outputs.BUILD_TAGS }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
@@ -270,7 +289,7 @@ jobs:
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: digests-linux-arm64
|
||||
name: digests-alpine-linux-arm64
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
@@ -282,26 +301,26 @@ jobs:
|
||||
- image-build-arm
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
||||
id: build
|
||||
with:
|
||||
platforms: linux/arm64
|
||||
@@ -309,13 +328,15 @@ jobs:
|
||||
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
|
||||
cache-from: type=gha,scope=buildkit-distroless-arm64
|
||||
cache-to: type=gha,mode=max,scope=buildkit-distroless-arm64
|
||||
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 }}
|
||||
LDFLAGS=-s -w
|
||||
BUILD_TAGS=${{ needs.generate-metadata.outputs.BUILD_TAGS }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
@@ -341,22 +362,22 @@ jobs:
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digests-*
|
||||
pattern: digests-alpine-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
flavor: |
|
||||
@@ -386,18 +407,18 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
flavor: |
|
||||
@@ -428,6 +449,6 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
|
||||
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3
|
||||
with:
|
||||
files: binaries/*
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -38,6 +38,6 @@ jobs:
|
||||
retention-days: 5
|
||||
|
||||
- name: Upload to code-scanning
|
||||
uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4
|
||||
uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Generate Sponsors
|
||||
uses: JamesIves/github-sponsors-readme-action@2fd9142e765f755780202122261dc85e78459405 # v1
|
||||
|
||||
@@ -51,3 +51,6 @@ config.certify.yml
|
||||
|
||||
# deepsec
|
||||
/.deepsec
|
||||
|
||||
# jetbrains
|
||||
/.idea/
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ Contributing to Tinyauth is straightforward. Follow the steps below to set up a
|
||||
## Requirements
|
||||
|
||||
- pnpm
|
||||
- Golang v1.24.0 or later
|
||||
- Golang v1.26.4 or later
|
||||
- Git
|
||||
- Docker
|
||||
- Make
|
||||
|
||||
+14
-9
@@ -1,5 +1,5 @@
|
||||
# Site builder
|
||||
FROM node:26.1-alpine3.23 AS frontend-builder
|
||||
FROM node:26.4-alpine3.23 AS frontend-builder
|
||||
|
||||
WORKDIR /frontend
|
||||
|
||||
@@ -27,6 +27,8 @@ FROM golang:1.26-alpine3.23 AS builder
|
||||
ARG VERSION
|
||||
ARG COMMIT_HASH
|
||||
ARG BUILD_TIMESTAMP
|
||||
ARG LDFLAGS
|
||||
ARG BUILD_TAGS
|
||||
|
||||
WORKDIR /tinyauth
|
||||
|
||||
@@ -39,27 +41,30 @@ COPY ./cmd ./cmd
|
||||
COPY ./internal ./internal
|
||||
COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
|
||||
|
||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w \
|
||||
RUN CGO_ENABLED=0 go build -tags "${BUILD_TAGS}" -ldflags "${LDFLAGS} \
|
||||
-X github.com/tinyauthapp/tinyauth/internal/model.Version=${VERSION} \
|
||||
-X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${COMMIT_HASH} \
|
||||
-X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth
|
||||
-X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${BUILD_TIMESTAMP}" \
|
||||
-o tinyauth ./cmd/tinyauth
|
||||
|
||||
# Runner
|
||||
FROM alpine:3.23 AS runner
|
||||
FROM alpine:3.24 AS runner
|
||||
|
||||
WORKDIR /tinyauth
|
||||
|
||||
COPY --from=builder /tinyauth/tinyauth ./
|
||||
|
||||
RUN mkdir -p /data
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
# Make the data directory with a non-root user
|
||||
RUN addgroup tinyauth && adduser -DH tinyauth -G tinyauth
|
||||
RUN mkdir -p /data/resources /data/oidc /data/tailscale
|
||||
RUN chown -R tinyauth:tinyauth /data
|
||||
|
||||
VOLUME ["/data"]
|
||||
|
||||
ENV TINYAUTH_DATABASE_PATH=/data/tinyauth.db
|
||||
|
||||
ENV TINYAUTH_RESOURCES_PATH=/data/resources
|
||||
# Tell tinyauth that it's running in a container and where to find the data directory
|
||||
ENV RUNTIME_ENV=docker
|
||||
|
||||
ENV PATH=$PATH:/tinyauth
|
||||
|
||||
|
||||
+15
-10
@@ -1,5 +1,5 @@
|
||||
# Site builder
|
||||
FROM node:26.1-alpine3.23 AS frontend-builder
|
||||
FROM node:26.4-alpine3.23 AS frontend-builder
|
||||
|
||||
WORKDIR /frontend
|
||||
|
||||
@@ -27,6 +27,8 @@ FROM golang:1.26-alpine3.23 AS builder
|
||||
ARG VERSION
|
||||
ARG COMMIT_HASH
|
||||
ARG BUILD_TIMESTAMP
|
||||
ARG LDFLAGS
|
||||
ARG BUILD_TAGS
|
||||
|
||||
WORKDIR /tinyauth
|
||||
|
||||
@@ -35,16 +37,20 @@ COPY go.sum ./
|
||||
|
||||
RUN go mod download
|
||||
|
||||
COPY ./cmd ./cmd
|
||||
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 \
|
||||
RUN CGO_ENABLED=0 go build -tags "${BUILD_TAGS}" -ldflags "${LDFLAGS} \
|
||||
-X github.com/tinyauthapp/tinyauth/internal/model.Version=${VERSION} \
|
||||
-X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${COMMIT_HASH} \
|
||||
-X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth
|
||||
-X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${BUILD_TIMESTAMP}" \
|
||||
-o tinyauth ./cmd/tinyauth
|
||||
|
||||
# Make the data directory with a non-root user
|
||||
RUN addgroup tinyauth && adduser -DH tinyauth -G tinyauth
|
||||
RUN mkdir -p /data/resources /data/oidc /data/tailscale
|
||||
RUN chown -R tinyauth:tinyauth /data
|
||||
|
||||
# Runner
|
||||
FROM gcr.io/distroless/static-debian12:latest AS runner
|
||||
@@ -54,15 +60,14 @@ 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
|
||||
COPY --from=builder /data /data
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
VOLUME ["/data"]
|
||||
|
||||
ENV TINYAUTH_DATABASE_PATH=/data/tinyauth.db
|
||||
|
||||
ENV TINYAUTH_RESOURCES_PATH=/data/resources
|
||||
# Tell tinyauth that it's running in a container and where to find the data directory
|
||||
ENV RUNTIME_ENV=docker
|
||||
|
||||
ENV PATH=$PATH:/tinyauth
|
||||
|
||||
|
||||
@@ -8,6 +8,10 @@ TAG_NAME := $(shell git describe --abbrev=0 --exact-match 2> /dev/null || echo "
|
||||
COMMIT_HASH := $(shell git rev-parse HEAD)
|
||||
BUILD_TIMESTAMP := $(shell date '+%Y-%m-%dT%H:%M:%S')
|
||||
BIN_NAME := tinyauth-$(GOARCH)
|
||||
LDFLAGS := -s -w
|
||||
# Whatever 6MB serialization lib Gin is using
|
||||
GIN_BUILD_TAGS := nomsgpack
|
||||
BUILD_TAGS = $(GIN_BUILD_TAGS)
|
||||
|
||||
# Development vars
|
||||
DEV_COMPOSE := $(shell test -f "docker-compose.test.yml" && echo "docker-compose.test.yml" || echo "docker-compose.dev.yml" )
|
||||
@@ -15,6 +19,8 @@ PROD_COMPOSE := $(shell test -f "docker-compose.test.prod.yml" && echo "docker-c
|
||||
|
||||
.DEFAULT_GOAL := binary
|
||||
|
||||
.PHONY: deps clean-data clean-webui webui binary binary-linux-amd64 binary-linux-arm64 test vet test-race dev dev-infisical prod prod-infisical sql generate docker docker-distroless
|
||||
|
||||
# Deps
|
||||
deps:
|
||||
cd frontend && pnpm ci
|
||||
@@ -36,7 +42,7 @@ webui: clean-webui
|
||||
|
||||
# Build the binary
|
||||
binary: webui
|
||||
CGO_ENABLED=$(CGO_ENABLED) go build -ldflags "-s -w \
|
||||
CGO_ENABLED=$(CGO_ENABLED) go build -tags "${BUILD_TAGS}" -ldflags "${LDFLAGS} \
|
||||
-X github.com/tinyauthapp/tinyauth/internal/model.Version=${TAG_NAME} \
|
||||
-X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${COMMIT_HASH} \
|
||||
-X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${BUILD_TIMESTAMP}" \
|
||||
@@ -57,10 +63,17 @@ binary-linux-arm64:
|
||||
$(MAKE) binary
|
||||
|
||||
# Go test
|
||||
.PHONY: test
|
||||
test:
|
||||
go test -v ./...
|
||||
|
||||
# Go vet
|
||||
vet:
|
||||
go vet ./...
|
||||
|
||||
# Go race
|
||||
test-race:
|
||||
go test -race ./...
|
||||
|
||||
# Development
|
||||
dev:
|
||||
docker compose -f $(DEV_COMPOSE) up --force-recreate --pull=always --remove-orphans --build
|
||||
@@ -78,11 +91,27 @@ prod-infisical:
|
||||
infisical run --env=dev -- docker compose -f $(PROD_COMPOSE) up --force-recreate --pull=always --remove-orphans
|
||||
|
||||
# SQL
|
||||
.PHONY: sql
|
||||
sql:
|
||||
sqlc generate
|
||||
|
||||
# Go gen
|
||||
generate:
|
||||
go run ./gen
|
||||
go generate ./internal/repository/...
|
||||
go generate ./...
|
||||
|
||||
# Docker image
|
||||
docker:
|
||||
docker buildx build -t tinyauthapp/tinyauth:dev \
|
||||
--build-arg=VERSION=$(TAG_NAME) \
|
||||
--build-arg=COMMIT_HASH=$(COMMIT_HASH) \
|
||||
--build-arg=BUILD_TIMESTAMP=$(BUILD_TIMESTAMP) \
|
||||
--build-arg=BUILD_TAGS=$(BUILD_TAGS) \
|
||||
-f Dockerfile .
|
||||
|
||||
# Docker image distroless
|
||||
docker-distroless:
|
||||
docker buildx build -t tinyauthapp/tinyauth:dev-distroless \
|
||||
--build-arg=VERSION=$(TAG_NAME) \
|
||||
--build-arg=COMMIT_HASH=$(COMMIT_HASH) \
|
||||
--build-arg=BUILD_TIMESTAMP=$(BUILD_TIMESTAMP) \
|
||||
--build-arg=BUILD_TAGS=$(BUILD_TAGS) \
|
||||
-f Dockerfile.distroless .
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div align="center">
|
||||
<img alt="Tinyauth" title="Tinyauth" width="96" src="assets/logo-rounded.png">
|
||||
<h1>Tinyauth</h1>
|
||||
<p>The tiniest authentication and authorization server you have ever seen.</p>
|
||||
<p>The tiniest OpenID Certified™ authorization and authentication server you have ever seen.</p>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
@@ -28,6 +28,10 @@ Tinyauth is the simplest and tiniest authentication and authorization server you
|
||||
> [!NOTE]
|
||||
> This is the main development branch. For the latest stable release, see the [documentation](https://tinyauth.app) or the latest stable tag.
|
||||
|
||||
As of 2026-06-25, Tinyauth v5.1.0 is OpenID Certified™ for Basic OP. You can find the certification details [here](https://openid.net/certification-old/certified-openid-providers-profiles/), test suite available [here](https://www.certification.openid.net/plan-detail.html?public=true&plan=H0qhpsOcQkxUE).
|
||||
|
||||
<img alt="OpenID Certified" width="200" src="https://openid.net/wordpress-content/uploads/2016/05/oid-l-certification-mark-l-cmyk-150dpi-90mm.jpg" />
|
||||
|
||||
## Getting Started
|
||||
|
||||
You can 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 (keep in mind that this file lives in the development branch so it may have updates that are not yet released).
|
||||
@@ -58,11 +62,20 @@ If you like, you can help translate Tinyauth into more languages by visiting the
|
||||
|
||||
Tinyauth is licensed under the GNU Affero General Public License v3.0. TL;DR — You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) AGPL-licensed code must also be made available under the AGPL along with build & install instructions. If you run a modified version over a network, you must also make the source available to the users of that service. For more information about the license check the [license](LICENSE) file.
|
||||
|
||||
|
||||
## Hosting Partners
|
||||
|
||||
If you use one of our partners, you can help support us while getting a great hosting deal.
|
||||
|
||||
<div>
|
||||
<a title="InstaPods" target="_blank" href="https://app.instapods.com/dashboard/pods/create?app=tinyauth&ref=tinyauth"><img src="https://instapods.com/deploy-button.svg"></a>
|
||||
</div>
|
||||
|
||||
## Sponsors
|
||||
|
||||
A big thank you to the following people for providing me with more coffee:
|
||||
|
||||
<!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https://github.com/erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a> <a href="https://github.com/nicotsx"><img src="https://github.com/nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a> <a href="https://github.com/SimpleHomelab"><img src="https://github.com/SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a> <a href="https://github.com/jmadden91"><img src="https://github.com/jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a> <a href="https://github.com/tribor"><img src="https://github.com/tribor.png" width="64px" alt="User avatar: tribor" /></a> <a href="https://github.com/eliasbenb"><img src="https://github.com/eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a> <a href="https://github.com/afunworm"><img src="https://github.com/afunworm.png" width="64px" alt="User avatar: afunworm" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="64px" alt="User avatar: chip-well" /></a> <a href="https://github.com/Lancelot-Enguerrand"><img src="https://github.com/Lancelot-Enguerrand.png" width="64px" alt="User avatar: Lancelot-Enguerrand" /></a> <a href="https://github.com/allgoewer"><img src="https://github.com/allgoewer.png" width="64px" alt="User avatar: allgoewer" /></a> <a href="https://github.com/NEANC"><img src="https://github.com/NEANC.png" width="64px" alt="User avatar: NEANC" /></a> <a href="https://github.com/ax-mad"><img src="https://github.com/ax-mad.png" width="64px" alt="User avatar: ax-mad" /></a> <a href="https://github.com/stegratech"><img src="https://github.com/stegratech.png" width="64px" alt="User avatar: stegratech" /></a> <a href="https://github.com/apearson"><img src="https://github.com/apearson.png" width="64px" alt="User avatar: apearson" /></a> <!-- sponsors -->
|
||||
<!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https://github.com/erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a> <a href="https://github.com/nicotsx"><img src="https://github.com/nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a> <a href="https://github.com/SimpleHomelab"><img src="https://github.com/SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a> <a href="https://github.com/jmadden91"><img src="https://github.com/jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a> <a href="https://github.com/tribor"><img src="https://github.com/tribor.png" width="64px" alt="User avatar: tribor" /></a> <a href="https://github.com/eliasbenb"><img src="https://github.com/eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a> <a href="https://github.com/afunworm"><img src="https://github.com/afunworm.png" width="64px" alt="User avatar: afunworm" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="64px" alt="User avatar: chip-well" /></a> <a href="https://github.com/Lancelot-Enguerrand"><img src="https://github.com/Lancelot-Enguerrand.png" width="64px" alt="User avatar: Lancelot-Enguerrand" /></a> <a href="https://github.com/allgoewer"><img src="https://github.com/allgoewer.png" width="64px" alt="User avatar: allgoewer" /></a> <a href="https://github.com/NEANC"><img src="https://github.com/NEANC.png" width="64px" alt="User avatar: NEANC" /></a> <a href="https://github.com/axjab"><img src="https://github.com/axjab.png" width="64px" alt="User avatar: axjab" /></a> <a href="https://github.com/stegratech"><img src="https://github.com/stegratech.png" width="64px" alt="User avatar: stegratech" /></a> <a href="https://github.com/apearson"><img src="https://github.com/apearson.png" width="64px" alt="User avatar: apearson" /></a> <a href="https://github.com/Micky5991"><img src="https://github.com/Micky5991.png" width="64px" alt="User avatar: Micky5991" /></a> <!-- sponsors -->
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/tinyauthapp/paerser/cli"
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
)
|
||||
|
||||
func configCmd(tconfig *model.Config, loaders []cli.ResourceLoader) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "config",
|
||||
Description: "Dump the current configuration in YAML format, useful for debugging",
|
||||
Configuration: tconfig,
|
||||
Resources: loaders,
|
||||
Run: func(_ []string) error {
|
||||
buf := strings.Builder{}
|
||||
|
||||
fmt.Fprint(&buf, "Your current configuration in YAML is:\n\n")
|
||||
|
||||
err := renderYamlToBuf(&buf, tconfig)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to render yaml config: %w", err)
|
||||
}
|
||||
|
||||
fmt.Print(buf.String())
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||
"github.com/tinyauthapp/paerser/cli"
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||
)
|
||||
|
||||
func createOidcClientCmd() *cli.Command {
|
||||
@@ -31,40 +32,84 @@ func createOidcClientCmd() *cli.Command {
|
||||
return errors.New("client name can only contain alphanumeric characters and hyphens")
|
||||
}
|
||||
|
||||
uuid := uuid.New()
|
||||
clientId := uuid.String()
|
||||
u := uuid.New()
|
||||
clientId := u.String()
|
||||
clientSecret := "ta-" + utils.GenerateString(61)
|
||||
|
||||
uclientName := strings.ToUpper(clientName)
|
||||
lclientName := strings.ToLower(clientName)
|
||||
|
||||
builder := strings.Builder{}
|
||||
buf := strings.Builder{}
|
||||
|
||||
// header
|
||||
fmt.Fprintf(&builder, "Created credentials for client %s\n\n", clientName)
|
||||
fmt.Fprintf(&buf, "Created '%s' OIDC client.\n\n", clientName)
|
||||
|
||||
// credentials
|
||||
fmt.Fprintf(&builder, "Client Name: %s\n", clientName)
|
||||
fmt.Fprintf(&builder, "Client ID: %s\n", clientId)
|
||||
fmt.Fprintf(&builder, "Client Secret: %s\n\n", clientSecret)
|
||||
fmt.Fprintf(&buf, "Credentials:\n\n")
|
||||
fmt.Fprintf(&buf, "Client Name: %s\n", clientName)
|
||||
fmt.Fprintf(&buf, "Client ID: %s\n", clientId)
|
||||
fmt.Fprintf(&buf, "Client Secret: %s\n\n", clientSecret)
|
||||
|
||||
// env variables
|
||||
fmt.Fprint(&builder, "Environment variables:\n\n")
|
||||
fmt.Fprintf(&builder, "TINYAUTH_OIDC_CLIENTS_%s_CLIENTID=%s\n", uclientName, clientId)
|
||||
fmt.Fprintf(&builder, "TINYAUTH_OIDC_CLIENTS_%s_CLIENTSECRET=%s\n", uclientName, clientSecret)
|
||||
fmt.Fprintf(&builder, "TINYAUTH_OIDC_CLIENTS_%s_NAME=%s\n\n", uclientName, utils.Capitalize(lclientName))
|
||||
// end variables
|
||||
fmt.Fprintf(&buf, "Environment variables:\n\n")
|
||||
renderToBuf(&buf, []kv{
|
||||
{
|
||||
k: fmt.Sprintf("TINYAUTH_OIDC_CLIENTS_%s_CLIENTID", uclientName),
|
||||
v: clientId,
|
||||
},
|
||||
{
|
||||
k: fmt.Sprintf("TINYAUTH_OIDC_CLIENTS_%s_CLIENTSECRET", uclientName),
|
||||
v: clientSecret,
|
||||
},
|
||||
{
|
||||
k: fmt.Sprintf("TINYAUTH_OIDC_CLIENTS_%s_NAME", uclientName),
|
||||
v: utils.Capitalize(lclientName),
|
||||
},
|
||||
}, "=")
|
||||
fmt.Fprintf(&buf, "\n")
|
||||
|
||||
// cli flags
|
||||
fmt.Fprint(&builder, "CLI flags:\n\n")
|
||||
fmt.Fprintf(&builder, "--oidc.clients.%s.clientid=%s\n", lclientName, clientId)
|
||||
fmt.Fprintf(&builder, "--oidc.clients.%s.clientsecret=%s\n", lclientName, clientSecret)
|
||||
fmt.Fprintf(&builder, "--oidc.clients.%s.name=%s\n\n", lclientName, utils.Capitalize(lclientName))
|
||||
fmt.Fprintf(&buf, "CLI flags:\n\n")
|
||||
renderToBuf(&buf, []kv{
|
||||
{
|
||||
k: fmt.Sprintf("--oidc.clients.%s.clientid", lclientName),
|
||||
v: clientId,
|
||||
},
|
||||
{
|
||||
k: fmt.Sprintf("--oidc.clients.%s.clientsecret", lclientName),
|
||||
v: clientSecret,
|
||||
},
|
||||
{
|
||||
k: fmt.Sprintf("--oidc.clients.%s.name", lclientName),
|
||||
v: utils.Capitalize(lclientName),
|
||||
},
|
||||
}, "=")
|
||||
fmt.Fprintf(&buf, "\n")
|
||||
|
||||
// yaml config
|
||||
fmt.Fprintf(&buf, "YAML config:\n\n")
|
||||
|
||||
err = renderYamlToBuf(&buf, &model.OIDCConfig{
|
||||
Clients: map[string]model.OIDCClientConfig{
|
||||
lclientName: {
|
||||
ClientID: clientId,
|
||||
ClientSecret: clientSecret,
|
||||
Name: utils.Capitalize(lclientName),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to render yaml config: %w", err)
|
||||
}
|
||||
|
||||
buf.WriteString("\n")
|
||||
|
||||
// footer
|
||||
fmt.Fprintln(&builder, "You can use either option to configure your OIDC client. Make sure to save these credentials as there is no way to regenerate them.")
|
||||
fmt.Fprintln(&buf, "You can use any of the above options to configure your OIDC client. Make sure to save these credentials as there is no way to regenerate them.")
|
||||
|
||||
// print
|
||||
out := builder.String()
|
||||
out := buf.String()
|
||||
fmt.Print(out)
|
||||
return nil
|
||||
},
|
||||
|
||||
+61
-15
@@ -3,11 +3,12 @@ package main
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"github.com/tinyauthapp/paerser/cli"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
@@ -34,62 +35,107 @@ func createUserCmd() *cli.Command {
|
||||
&cli.FlagLoader{},
|
||||
}
|
||||
|
||||
return &cli.Command{
|
||||
cmd := &cli.Command{
|
||||
Name: "create",
|
||||
Description: "Create a user",
|
||||
Configuration: tCfg,
|
||||
Resources: loaders,
|
||||
Run: func(_ []string) error {
|
||||
log := logger.NewLogger().WithSimpleConfig()
|
||||
log.Init()
|
||||
}
|
||||
|
||||
cmd.Run = func(_ []string) error {
|
||||
if tCfg.Interactive {
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().Title("Username").Value(&tCfg.Username).Validate((func(s string) error {
|
||||
huh.NewInput().Title("Username").Value(&tCfg.Username).Validate(func(s string) error {
|
||||
if s == "" {
|
||||
return errors.New("username cannot be empty")
|
||||
}
|
||||
if strings.Contains(s, ":") {
|
||||
return errors.New("username cannot contain ':'")
|
||||
}
|
||||
return nil
|
||||
})),
|
||||
huh.NewInput().Title("Password").Value(&tCfg.Password).Validate((func(s string) error {
|
||||
}),
|
||||
huh.NewInput().Title("Password").Value(&tCfg.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(&tCfg.Docker),
|
||||
),
|
||||
)
|
||||
|
||||
theme := new(themeBase)
|
||||
err := form.WithTheme(theme).Run()
|
||||
|
||||
err := form.WithTheme(theme).Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run interactive prompt: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if tCfg.Username == "" || tCfg.Password == "" {
|
||||
cmd.PrintHelp(os.Stdout)
|
||||
return errors.New("username and password cannot be empty")
|
||||
}
|
||||
|
||||
log.App.Info().Str("username", tCfg.Username).Msg("Creating user")
|
||||
if strings.Contains(tCfg.Username, ":") {
|
||||
return errors.New("username cannot contain ':'")
|
||||
}
|
||||
|
||||
passwd, err := bcrypt.GenerateFromPassword([]byte(tCfg.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to hash password: %w", err)
|
||||
}
|
||||
|
||||
// If docker format is enabled, escape the dollar sign
|
||||
// Only the docker compose output needs $ escaped, the raw hash is correct everywhere else
|
||||
passwdStr := string(passwd)
|
||||
outputStr := passwdStr
|
||||
|
||||
if tCfg.Docker {
|
||||
passwdStr = strings.ReplaceAll(passwdStr, "$", "$$")
|
||||
outputStr = strings.ReplaceAll(passwdStr, "$", "$$")
|
||||
}
|
||||
|
||||
log.App.Info().Str("user", fmt.Sprintf("%s:%s", tCfg.Username, passwdStr)).Msg("User created")
|
||||
user := fmt.Sprintf("%s:%s", tCfg.Username, passwdStr)
|
||||
escapedUser := fmt.Sprintf("%s:%s", tCfg.Username, outputStr)
|
||||
|
||||
return nil
|
||||
buf := strings.Builder{}
|
||||
|
||||
// header
|
||||
fmt.Fprintf(&buf, "Created user '%s'.\n\n", tCfg.Username)
|
||||
|
||||
// environment variable
|
||||
fmt.Fprint(&buf, "Environment variable:\n\n")
|
||||
renderToBuf(&buf, []kv{
|
||||
{"TINYAUTH_AUTH_USERS", escapedUser},
|
||||
}, "=")
|
||||
|
||||
// cli flags
|
||||
fmt.Fprint(&buf, "\nCLI flags:\n\n")
|
||||
renderToBuf(&buf, []kv{
|
||||
{"--auth.users", user},
|
||||
}, "=")
|
||||
|
||||
// yaml config
|
||||
fmt.Fprint(&buf, "\nYAML config:\n\n")
|
||||
|
||||
err = renderYamlToBuf(&buf, &model.Config{
|
||||
Auth: model.AuthConfig{
|
||||
Users: []string{user},
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to render yaml config: %w", err)
|
||||
}
|
||||
|
||||
buf.WriteString("\n")
|
||||
|
||||
// footer
|
||||
fmt.Fprint(&buf, "Use your config option of choice to add the user to Tinyauth and then restart.")
|
||||
|
||||
fmt.Println(buf.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"github.com/mdp/qrterminal/v3"
|
||||
@@ -34,14 +33,15 @@ func generateTotpCmd() *cli.Command {
|
||||
&cli.FlagLoader{},
|
||||
}
|
||||
|
||||
return &cli.Command{
|
||||
cmd := &cli.Command{
|
||||
Name: "generate",
|
||||
Description: "Generate a TOTP secret",
|
||||
Configuration: tCfg,
|
||||
Resources: loaders,
|
||||
Run: func(_ []string) error {
|
||||
log := logger.NewLogger().WithSimpleConfig()
|
||||
log.Init()
|
||||
}
|
||||
|
||||
cmd.Run = func(_ []string) error {
|
||||
colors := getColors()
|
||||
|
||||
if tCfg.Interactive {
|
||||
form := huh.NewForm(
|
||||
@@ -63,6 +63,11 @@ func generateTotpCmd() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
if tCfg.User == "" {
|
||||
cmd.PrintHelp(os.Stdout)
|
||||
return fmt.Errorf("user is required")
|
||||
}
|
||||
|
||||
user, err := utils.ParseUser(tCfg.User)
|
||||
|
||||
if err != nil {
|
||||
@@ -89,9 +94,7 @@ func generateTotpCmd() *cli.Command {
|
||||
|
||||
secret := key.Secret()
|
||||
|
||||
log.App.Info().Str("secret", secret).Msg("Generated TOTP secret")
|
||||
|
||||
log.App.Info().Msg("Generated QR code")
|
||||
fmt.Printf("Scan the following QR code with your authenticator app (e.g., Google Authenticator, 2fauth, Microsoft Authenticator):\n\n")
|
||||
|
||||
config := qrterminal.Config{
|
||||
Level: qrterminal.L,
|
||||
@@ -110,9 +113,18 @@ func generateTotpCmd() *cli.Command {
|
||||
user.Password = strings.ReplaceAll(user.Password, "$", "$$")
|
||||
}
|
||||
|
||||
log.App.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.")
|
||||
userStr := fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TOTPSecret)
|
||||
|
||||
fmt.Print("\nOr add the following TOTP secret to your authenticator app: ")
|
||||
fmt.Print(colors.green.Render(secret))
|
||||
fmt.Print("\n\n")
|
||||
|
||||
fmt.Printf("Finally, add your user '%s' back to your configuration: ", user.Username)
|
||||
fmt.Print(colors.green.Render(userStr))
|
||||
fmt.Print("\n")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
+147
-16
@@ -2,18 +2,23 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/loaders"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/tinyauthapp/paerser/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
tConfig := model.NewDefaultConfiguration()
|
||||
env := model.DetectRuntimeEnv()
|
||||
tConfig := model.NewDefaultConfiguration(env)
|
||||
|
||||
loaders := []cli.ResourceLoader{
|
||||
&loaders.FileLoader{},
|
||||
@@ -27,83 +32,114 @@ func main() {
|
||||
Configuration: tConfig,
|
||||
Resources: loaders,
|
||||
Run: func(_ []string) error {
|
||||
if !reflect.DeepEqual(model.NewDefaultConfiguration(env).Experimental, tConfig.Experimental) {
|
||||
colors := getColors()
|
||||
fmt.Println(colors.yellow.Render("⚠") + " Experimental features are enabled, use with caution. Experimental features may change with each release.")
|
||||
}
|
||||
return runCmd(*tConfig)
|
||||
},
|
||||
}
|
||||
|
||||
cmdUser := &cli.Command{
|
||||
Name: "user",
|
||||
Description: "Manage Tinyauth users",
|
||||
Description: "Manage users",
|
||||
}
|
||||
|
||||
cmdTotp := &cli.Command{
|
||||
Name: "totp",
|
||||
Description: "Manage Tinyauth TOTP users",
|
||||
Description: "Manage TOTP users",
|
||||
}
|
||||
|
||||
cmdOidc := &cli.Command{
|
||||
Name: "oidc",
|
||||
Description: "Manage Tinyauth OIDC clients",
|
||||
Description: "Manage OIDC clients",
|
||||
}
|
||||
|
||||
err := cmdTinyauth.AddCommand(versionCmd())
|
||||
helpCmd := &cli.Command{
|
||||
Name: "help",
|
||||
Description: "Show the help message",
|
||||
Run: func(_ []string) error {
|
||||
return cmdTinyauth.PrintHelp(os.Stdout)
|
||||
},
|
||||
}
|
||||
|
||||
err := cmdTinyauth.AddCommand(helpCmd)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to add version command")
|
||||
fatalf(err, "Failed to add help command")
|
||||
}
|
||||
|
||||
err = cmdTinyauth.AddCommand(versionCmd())
|
||||
|
||||
if err != nil {
|
||||
fatalf(err, "Failed to add version command")
|
||||
}
|
||||
|
||||
err = cmdTinyauth.AddCommand(configCmd(tConfig, loaders))
|
||||
|
||||
if err != nil {
|
||||
fatalf(err, "Failed to add config command")
|
||||
}
|
||||
|
||||
err = cmdUser.AddCommand(verifyUserCmd())
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to add verify command")
|
||||
fatalf(err, "Failed to add user verify command")
|
||||
}
|
||||
|
||||
err = cmdTinyauth.AddCommand(healthcheckCmd())
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to add healthcheck command")
|
||||
fatalf(err, "Failed to add healthcheck command")
|
||||
}
|
||||
|
||||
err = cmdTotp.AddCommand(generateTotpCmd())
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to add generate command")
|
||||
fatalf(err, "Failed to add totp generate command")
|
||||
}
|
||||
|
||||
err = cmdUser.AddCommand(createUserCmd())
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to add create command")
|
||||
fatalf(err, "Failed to add create user command")
|
||||
}
|
||||
|
||||
err = cmdOidc.AddCommand(createOidcClientCmd())
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to add create command")
|
||||
fatalf(err, "Failed to add create oidc client command")
|
||||
}
|
||||
|
||||
err = cmdTinyauth.AddCommand(cmdUser)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to add user command")
|
||||
fatalf(err, "Failed to add user command")
|
||||
}
|
||||
|
||||
err = cmdTinyauth.AddCommand(cmdTotp)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to add totp command")
|
||||
fatalf(err, "Failed to add totp command")
|
||||
}
|
||||
|
||||
err = cmdTinyauth.AddCommand(cmdOidc)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to add oidc command")
|
||||
fatalf(err, "Failed to add oidc command")
|
||||
}
|
||||
|
||||
err = cli.Execute(cmdTinyauth)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to execute command")
|
||||
if strings.Contains(err.Error(), "command not found") {
|
||||
fmt.Println("Command not found. Use 'tinyauth help' to see available commands.")
|
||||
return
|
||||
}
|
||||
if strings.Contains(err.Error(), "is not runnable") {
|
||||
return
|
||||
}
|
||||
fatalf(err, "Failed to execute command")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,3 +160,98 @@ type themeBase struct{}
|
||||
func (t *themeBase) Theme(isDark bool) *huh.Styles {
|
||||
return huh.ThemeBase(isDark)
|
||||
}
|
||||
|
||||
type colors struct {
|
||||
blue lipgloss.Style
|
||||
gray lipgloss.Style
|
||||
lightGray lipgloss.Style
|
||||
green lipgloss.Style
|
||||
yellow lipgloss.Style
|
||||
}
|
||||
|
||||
func getColors() colors {
|
||||
noColor := os.Getenv("NO_COLOR")
|
||||
forceColor := os.Getenv("FORCE_COLOR")
|
||||
|
||||
colorOut := colors{
|
||||
green: lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(34)),
|
||||
gray: lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(245)),
|
||||
yellow: lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(214)),
|
||||
blue: lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(75)),
|
||||
lightGray: lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(250)),
|
||||
}
|
||||
|
||||
noColorOut := colors{
|
||||
green: lipgloss.NewStyle(),
|
||||
gray: lipgloss.NewStyle(),
|
||||
yellow: lipgloss.NewStyle(),
|
||||
blue: lipgloss.NewStyle(),
|
||||
lightGray: lipgloss.NewStyle(),
|
||||
}
|
||||
|
||||
useColors := true
|
||||
|
||||
if noColor == "true" || noColor == "1" {
|
||||
useColors = false
|
||||
}
|
||||
|
||||
if forceColor == "true" || forceColor == "1" {
|
||||
useColors = true
|
||||
}
|
||||
|
||||
if !useColors {
|
||||
return noColorOut
|
||||
}
|
||||
|
||||
return colorOut
|
||||
}
|
||||
|
||||
func fatalf(err error, msg string) {
|
||||
fmt.Printf("%s: %v\n", msg, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
type kv struct {
|
||||
k string
|
||||
v string
|
||||
}
|
||||
|
||||
func renderToBuf(buf *strings.Builder, kv []kv, sep string) {
|
||||
colors := getColors()
|
||||
for _, i := range kv {
|
||||
buf.WriteString(colors.blue.Render(i.k))
|
||||
buf.WriteString(colors.gray.Render(sep))
|
||||
buf.WriteString(colors.lightGray.Render(i.v))
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
func renderYamlToBuf(buf *strings.Builder, i any) error {
|
||||
colors := getColors()
|
||||
|
||||
yout, err := yaml.Marshal(i)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal yaml: %w", err)
|
||||
}
|
||||
|
||||
for l := range strings.SplitSeq(string(yout), "\n") {
|
||||
if l == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(strings.TrimLeft(l, " "), "- ") {
|
||||
buf.WriteString(colors.lightGray.Render(l))
|
||||
buf.WriteString("\n")
|
||||
continue
|
||||
}
|
||||
lp := strings.SplitN(l, ":", 2)
|
||||
buf.WriteString(colors.blue.Render(lp[0]))
|
||||
buf.WriteString(colors.gray.Render(":"))
|
||||
if len(lp) == 2 {
|
||||
buf.WriteString(colors.lightGray.Render(lp[1]))
|
||||
}
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+16
-10
@@ -3,9 +3,9 @@ package main
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"github.com/pquerna/otp/totp"
|
||||
@@ -38,14 +38,15 @@ func verifyUserCmd() *cli.Command {
|
||||
&cli.FlagLoader{},
|
||||
}
|
||||
|
||||
return &cli.Command{
|
||||
cmd := &cli.Command{
|
||||
Name: "verify",
|
||||
Description: "Verify a user is set up correctly",
|
||||
Configuration: tCfg,
|
||||
Resources: loaders,
|
||||
Run: func(_ []string) error {
|
||||
log := logger.NewLogger().WithSimpleConfig()
|
||||
log.Init()
|
||||
}
|
||||
|
||||
cmd.Run = func(_ []string) error {
|
||||
colors := getColors()
|
||||
|
||||
if tCfg.Interactive {
|
||||
form := huh.NewForm(
|
||||
@@ -80,6 +81,11 @@ func verifyUserCmd() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
if tCfg.User == "" || tCfg.Username == "" || tCfg.Password == "" {
|
||||
cmd.PrintHelp(os.Stdout)
|
||||
return fmt.Errorf("user, username, and password are required")
|
||||
}
|
||||
|
||||
user, err := utils.ParseUser(tCfg.User)
|
||||
|
||||
if err != nil {
|
||||
@@ -98,21 +104,21 @@ func verifyUserCmd() *cli.Command {
|
||||
|
||||
if user.TOTPSecret == "" {
|
||||
if tCfg.Totp != "" {
|
||||
log.App.Warn().Msg("User does not have TOTP secret")
|
||||
fmt.Println(colors.yellow.Render("⚠") + " TOTP code provided but user does not have TOTP enabled")
|
||||
}
|
||||
log.App.Info().Msg("User verified")
|
||||
fmt.Println(colors.green.Render("✓") + " User verified")
|
||||
return nil
|
||||
}
|
||||
|
||||
ok := totp.Validate(tCfg.Totp, user.TOTPSecret)
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("TOTP code incorrect")
|
||||
}
|
||||
|
||||
log.App.Info().Msg("User verified")
|
||||
fmt.Println(colors.green.Render("✓") + " User verified")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -14,9 +14,10 @@ func versionCmd() *cli.Command {
|
||||
Configuration: nil,
|
||||
Resources: nil,
|
||||
Run: func(_ []string) error {
|
||||
fmt.Printf("Version: %s\n", model.Version)
|
||||
fmt.Printf("Commit Hash: %s\n", model.CommitHash)
|
||||
fmt.Printf("Build Timestamp: %s\n", model.BuildTimestamp)
|
||||
colors := getColors()
|
||||
fmt.Printf("Version: %s\n", colors.blue.Render(model.Version))
|
||||
fmt.Printf("Commit Hash: %s\n", colors.blue.Render(model.CommitHash))
|
||||
fmt.Printf("Build Timestamp: %s\n", colors.blue.Render(model.BuildTimestamp))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
+27
-28
@@ -12,50 +12,49 @@
|
||||
},
|
||||
"packageManager": "pnpm@11.1.2",
|
||||
"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.2.2",
|
||||
"@tanstack/react-query": "^5.99.0",
|
||||
"axios": "^1.15.0",
|
||||
"@hookform/resolvers": "^5.4.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.19",
|
||||
"@radix-ui/react-label": "^2.1.11",
|
||||
"@radix-ui/react-select": "^2.3.2",
|
||||
"@radix-ui/react-separator": "^1.1.11",
|
||||
"@radix-ui/react-slot": "^1.3.0",
|
||||
"@tailwindcss/vite": "^4.3.2",
|
||||
"@tanstack/react-query": "^5.101.2",
|
||||
"axios": "^1.18.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"i18next": "^26.0.4",
|
||||
"i18next": "^26.3.4",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"lucide-react": "^1.8.0",
|
||||
"lucide-react": "^1.23.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"react-hook-form": "^7.72.1",
|
||||
"react-i18next": "^17.0.2",
|
||||
"radix-ui": "^1.6.1",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-hook-form": "^7.81.0",
|
||||
"react-i18next": "^17.0.8",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router": "^7.14.0",
|
||||
"react-router": "^8.1.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"tailwindcss": "^4.3.2",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@tanstack/eslint-plugin-query": "^5.99.0",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@tanstack/eslint-plugin-query": "^5.101.2",
|
||||
"@types/node": "^26.1.0",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"eslint": "^10.2.0",
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
"eslint": "^10.6.0",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.5.0",
|
||||
"prettier": "3.8.2",
|
||||
"eslint-plugin-react-refresh": "^0.5.3",
|
||||
"globals": "^17.7.0",
|
||||
"rollup-plugin-visualizer": "^7.0.1",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~6.0.2",
|
||||
"typescript-eslint": "^8.58.1",
|
||||
"vite": "^8.0.8"
|
||||
"typescript-eslint": "^8.62.1",
|
||||
"vite": "^8.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+1309
-1404
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
||||
dangerouslyAllowAllBuilds: false
|
||||
blockExoticSubdeps: true
|
||||
minimumReleaseAge: 1440 # 1 day
|
||||
trustPolicy: no-downgrade
|
||||
|
||||
@@ -78,7 +78,7 @@ export const LoginForm = (props: Props) => {
|
||||
`/forgot-password${props.params ? `${props.params}` : ""}`,
|
||||
);
|
||||
}}
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm absolute right-0 bottom-[2.565rem]" // 2.565 is *just* perfect
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm absolute right-0 bottom-[2.565rem] max-w-40 truncate" // 2.565 is *just* perfect
|
||||
>
|
||||
{t("forgotPasswordTitle")}
|
||||
</a>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function LocalAuthIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0-8 0M6 21v-2a4 4 0 0 1 4-4h5m3.5 3.5L15 22l-1.5-1.5m5.054-2.086a2 2 0 1 1 2.828-2.828a2 2 0 0 1-2.828 2.828M16 19l1 1"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { languages, SupportedLanguage } from "@/lib/i18n/locales";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../ui/select";
|
||||
import { useState } from "react";
|
||||
import i18n from "@/lib/i18n/i18n";
|
||||
|
||||
export const LanguageSelector = () => {
|
||||
const [language, setLanguage] = useState<SupportedLanguage>(
|
||||
i18n.language as SupportedLanguage,
|
||||
);
|
||||
|
||||
const handleSelect = (option: string) => {
|
||||
setLanguage(option as SupportedLanguage);
|
||||
i18n.changeLanguage(option as SupportedLanguage);
|
||||
};
|
||||
|
||||
return (
|
||||
<Select onValueChange={handleSelect} value={language}>
|
||||
<SelectTrigger aria-label="Select language">
|
||||
<SelectValue placeholder="Select language" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(languages).map(([key, value]) => (
|
||||
<SelectItem key={key} value={key}>
|
||||
{value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useAppContext } from "@/context/app-context";
|
||||
import { LanguageSelector } from "../language/language";
|
||||
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 { QuickActions } from "../quick-actions/quick-actions";
|
||||
import { isTrustedDomain } from "@/lib/hooks/redirect-uri";
|
||||
|
||||
const BaseLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
const { ui } = useAppContext();
|
||||
@@ -21,9 +21,8 @@ const BaseLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
backgroundPosition: "center",
|
||||
}}
|
||||
>
|
||||
<div className="absolute top-4 right-4 flex flex-row gap-2">
|
||||
<ThemeToggle />
|
||||
<LanguageSelector />
|
||||
<div className="absolute top-4 right-4">
|
||||
<QuickActions />
|
||||
</div>
|
||||
<div className="max-w-sm md:min-w-sm min-w-xs">{children}</div>
|
||||
</div>
|
||||
@@ -42,11 +41,18 @@ export const Layout = () => {
|
||||
setIgnoreDomainWarning(true);
|
||||
}, [setIgnoreDomainWarning]);
|
||||
|
||||
if (
|
||||
!ignoreDomainWarning &&
|
||||
ui.warningsEnabled &&
|
||||
!app.trustedDomains.includes(currentUrl)
|
||||
) {
|
||||
const isTrusted = (() => {
|
||||
try {
|
||||
const appUrlObj = new URL(app.appUrl);
|
||||
const currentUrlObj = new URL(currentUrl);
|
||||
|
||||
return isTrustedDomain(currentUrlObj, appUrlObj, "", false);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!ignoreDomainWarning && ui.warningsEnabled && !isTrusted) {
|
||||
return (
|
||||
<BaseLayout>
|
||||
<DomainWarning
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
import { languages, SupportedLanguage } from "@/lib/i18n/locales";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "../ui/dropdown-menu";
|
||||
import { useState } from "react";
|
||||
import i18n from "@/lib/i18n/i18n";
|
||||
import { useUserContext } from "@/context/user-context";
|
||||
import { ScrollArea } from "../ui/scroll-area";
|
||||
import { useTheme } from "../providers/theme-provider";
|
||||
import {
|
||||
Check,
|
||||
DoorOpenIcon,
|
||||
Languages,
|
||||
Monitor,
|
||||
Moon,
|
||||
Palette,
|
||||
Settings,
|
||||
Sun,
|
||||
UserRoundKey,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation } from "react-router";
|
||||
import { useRef } from "react";
|
||||
import {
|
||||
useScreenParams,
|
||||
recompileScreenParams,
|
||||
} from "@/lib/hooks/screen-params";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
import { toast } from "sonner";
|
||||
import { useEffect } from "react";
|
||||
import { GoogleIcon } from "../icons/google";
|
||||
import { GithubIcon } from "../icons/github";
|
||||
import { TailscaleIcon } from "../icons/tailscale";
|
||||
import { MicrosoftIcon } from "../icons/microsoft";
|
||||
import { PocketIDIcon } from "../icons/pocket-id";
|
||||
import { OAuthIcon } from "../icons/oauth";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||
|
||||
const iconStyles = "size-4";
|
||||
|
||||
const iconMap: Record<string, React.ReactNode> = {
|
||||
google: <GoogleIcon className={iconStyles} />,
|
||||
github: <GithubIcon className={iconStyles} />,
|
||||
tailscale: <TailscaleIcon className={iconStyles} />,
|
||||
microsoft: <MicrosoftIcon className={iconStyles} />,
|
||||
pocketid: <PocketIDIcon className={iconStyles} />,
|
||||
};
|
||||
|
||||
export const QuickActions = () => {
|
||||
const { auth, oauth, tailscale } = useUserContext();
|
||||
const { theme, setTheme } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { search } = useLocation();
|
||||
|
||||
const [language, setLanguage] = useState<SupportedLanguage>(
|
||||
i18n.language as SupportedLanguage,
|
||||
);
|
||||
|
||||
const redirectTimer = useRef<number | null>(null);
|
||||
const searchParams = new URLSearchParams(search);
|
||||
const screenParams = useScreenParams(searchParams);
|
||||
const compiledParams = recompileScreenParams(screenParams);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const providerDetails = (():
|
||||
| { name: string; icon: React.ReactNode }
|
||||
| undefined => {
|
||||
if (!auth.authenticated) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (auth.providerId === "local" || auth.providerId === "ldap") {
|
||||
return {
|
||||
name: t(
|
||||
auth.providerId === "ldap"
|
||||
? "quickActionsProviderLDAP"
|
||||
: "quickActionsProviderLocal",
|
||||
),
|
||||
icon: (
|
||||
<UserRoundKey
|
||||
strokeWidth={1.5}
|
||||
size={16}
|
||||
className="text-muted-foreground ml-0.5"
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (oauth.active) {
|
||||
return {
|
||||
name: t("quickActionsProviderOAuth", { provider: oauth.displayName }),
|
||||
icon: iconMap[auth.providerId] || <OAuthIcon className={iconStyles} />,
|
||||
};
|
||||
}
|
||||
|
||||
if (auth.providerId === "tailscale") {
|
||||
return {
|
||||
name: `Tailscale (${tailscale.nodeName})`,
|
||||
icon: <TailscaleIcon className={iconStyles} />,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
})();
|
||||
|
||||
const logoutMutation = useMutation({
|
||||
mutationFn: () => axios.post("/api/user/logout"),
|
||||
mutationKey: ["logout"],
|
||||
onSuccess: () => {
|
||||
toast.success(t("logoutSuccessTitle"), {
|
||||
description: t("logoutSuccessSubtitle"),
|
||||
});
|
||||
|
||||
redirectTimer.current = window.setTimeout(() => {
|
||||
window.location.replace(`/login${compiledParams}`);
|
||||
}, 500);
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t("logoutFailTitle"), {
|
||||
description: t("logoutFailSubtitle"),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (redirectTimer.current) {
|
||||
clearTimeout(redirectTimer.current);
|
||||
}
|
||||
};
|
||||
}, [redirectTimer]);
|
||||
|
||||
const initial = auth.authenticated
|
||||
? (auth.name[0] || "U").toUpperCase()
|
||||
: null;
|
||||
|
||||
const handleSelect = (option: string) => {
|
||||
setLanguage(option as SupportedLanguage);
|
||||
i18n.changeLanguage(option as SupportedLanguage);
|
||||
};
|
||||
|
||||
const themes = [
|
||||
{ key: "light", label: t("quickActionsThemeLight"), icon: Sun },
|
||||
{ key: "dark", label: t("quickActionsThemeDark"), icon: Moon },
|
||||
{ key: "system", label: t("quickActionsThemeSystem"), icon: Monitor },
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<DropdownMenu onOpenChange={(open) => setIsOpen(open)} open={isOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
aria-label={t("quickActionsTitle")}
|
||||
className="rounded-full transition-transform duration-200 will-change-transform hover:scale-105 hover:cursor-pointer focus:ring-0 focus:outline-3 focus:outline-ring/50"
|
||||
>
|
||||
{auth.authenticated ? (
|
||||
<div className="size-10 flex justify-center items-center p-2 rounded-full bg-card border border-border">
|
||||
{isOpen ? (
|
||||
<X className="size-4 text-primary rotate-0 transition-transform duration-200 starting:rotate-45" />
|
||||
) : (
|
||||
<span className="text-sm text-primary rotate-0 transition-transform duration-200 starting:-rotate-45">
|
||||
{initial}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="bg-card text-primary border-border size-10 flex items-center justify-center rounded-full border shadow-lg">
|
||||
<Settings
|
||||
className={`size-4 transition-transform duration-200 ${
|
||||
isOpen ? "rotate-45" : "rotate-0"
|
||||
}`}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
sideOffset={8}
|
||||
className="rounded-xl p-1 w-3xs"
|
||||
>
|
||||
{auth.authenticated && (
|
||||
<>
|
||||
<DropdownMenuLabel className="flex items-center gap-3 p-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="size-9 rounded-full p-2 bg-muted border-border border flex items-center justify-center">
|
||||
{providerDetails!.icon}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{providerDetails!.name}</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className="flex min-w-0 flex-col gap-0.5">
|
||||
<span className="truncate text-sm font-medium">
|
||||
{auth.name}
|
||||
</span>
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
{auth.email}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Languages className="size-4" />
|
||||
{t("quickActionsLanguage")}
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent sideOffset={8} className="rounded-xl p-1">
|
||||
<ScrollArea className="h-80">
|
||||
{Object.entries(languages).map(([key, value]) => (
|
||||
<DropdownMenuItem
|
||||
key={key}
|
||||
onSelect={() => handleSelect(key)}
|
||||
>
|
||||
{value}
|
||||
{language === key && <Check className="size-4" />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</ScrollArea>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Palette className="size-4" />
|
||||
{t("quickActionsTheme")}
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent className="rounded-xl p-1" sideOffset={8}>
|
||||
{themes.map(({ key, label, icon: Icon }) => (
|
||||
<DropdownMenuItem key={key} onClick={() => setTheme(key)}>
|
||||
<span className="flex items-center gap-2">
|
||||
<Icon className="size-4" />
|
||||
{label}
|
||||
</span>
|
||||
{theme === key && <Check className="size-4" />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
|
||||
{auth.authenticated && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onSelect={() => logoutMutation.mutate()}
|
||||
className="text-destructive"
|
||||
>
|
||||
<DoorOpenIcon className="size-4 text-destructive" />
|
||||
{t("quickActionsLogout")}
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import * as React from "react"
|
||||
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none p-px transition-colors select-none",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="relative flex-1 rounded-full bg-border"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
@@ -0,0 +1,17 @@
|
||||
type UseLoginForProps = {
|
||||
login_for?: "oidc" | "app";
|
||||
compiledParams: string;
|
||||
};
|
||||
|
||||
export const useLoginFor = (props: UseLoginForProps): string => {
|
||||
const { login_for, compiledParams } = props;
|
||||
|
||||
switch (login_for) {
|
||||
case "oidc":
|
||||
return "/oidc/authorize" + compiledParams;
|
||||
case "app":
|
||||
return "/continue" + compiledParams;
|
||||
default:
|
||||
return "/logout";
|
||||
}
|
||||
};
|
||||
@@ -1,76 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const oidcParamsSchema = z.object({
|
||||
scope: z.string().min(1),
|
||||
response_type: z.string().min(1),
|
||||
client_id: z.string().min(1),
|
||||
redirect_uri: z.string().min(1),
|
||||
state: z.string().optional(),
|
||||
nonce: z.string().optional(),
|
||||
code_challenge: z.string().optional(),
|
||||
code_challenge_method: z.string().optional(),
|
||||
});
|
||||
|
||||
function b64urlDecode(s: string): string {
|
||||
const base64 = s.replace(/-/g, "+").replace(/_/g, "/");
|
||||
return atob(base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "="));
|
||||
}
|
||||
|
||||
function decodeRequestObject(jwt: string): Record<string, string> {
|
||||
try {
|
||||
// Must have exactly 3 parts: header, payload, signature
|
||||
const parts = jwt.split(".");
|
||||
if (parts.length !== 3) return {};
|
||||
|
||||
// Header must specify "alg": "none" and signature must be empty string
|
||||
const header = JSON.parse(b64urlDecode(parts[0]));
|
||||
if (!header || typeof header !== "object" || header.alg !== "none" || parts[2] !== "") return {};
|
||||
|
||||
const payload = JSON.parse(b64urlDecode(parts[1]));
|
||||
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return {};
|
||||
const result: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(payload)) {
|
||||
if (typeof v === "string") result[k] = v;
|
||||
}
|
||||
return result;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export const useOIDCParams = (
|
||||
params: URLSearchParams,
|
||||
): {
|
||||
values: z.infer<typeof oidcParamsSchema>;
|
||||
issues: string[];
|
||||
isOidc: boolean;
|
||||
compiled: string;
|
||||
} => {
|
||||
const obj = Object.fromEntries(params.entries());
|
||||
|
||||
// RFC 9101 / OIDC Core 6.1: if `request` param present, decode JWT payload
|
||||
// and merge claims over top-level params (JWT claims take precedence)
|
||||
const requestJwt = params.get("request");
|
||||
if (requestJwt) {
|
||||
const claims = decodeRequestObject(requestJwt);
|
||||
Object.assign(obj, claims);
|
||||
}
|
||||
|
||||
const parsed = oidcParamsSchema.safeParse(obj);
|
||||
|
||||
if (parsed.success) {
|
||||
return {
|
||||
values: parsed.data,
|
||||
issues: [],
|
||||
isOidc: true,
|
||||
compiled: new URLSearchParams(parsed.data).toString(),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
issues: parsed.error.issues.map((issue) => issue.path.toString()),
|
||||
values: {} as z.infer<typeof oidcParamsSchema>,
|
||||
isOidc: false,
|
||||
compiled: "",
|
||||
};
|
||||
};
|
||||
@@ -7,14 +7,29 @@ type IuseRedirectUri = {
|
||||
};
|
||||
|
||||
export const useRedirectUri = (
|
||||
redirect_uri: string | null,
|
||||
redirect_uri: string | undefined,
|
||||
cookieDomain: string,
|
||||
appUrl: string,
|
||||
subdomainsEnabled: boolean,
|
||||
): IuseRedirectUri => {
|
||||
let isValid = false;
|
||||
let isTrusted = false;
|
||||
let isAllowedProto = false;
|
||||
let isHttpsDowngrade = false;
|
||||
|
||||
let appUrlObj: URL;
|
||||
|
||||
try {
|
||||
appUrlObj = new URL(appUrl);
|
||||
} catch {
|
||||
return {
|
||||
valid: isValid,
|
||||
trusted: isTrusted,
|
||||
allowedProto: isAllowedProto,
|
||||
httpsDowngrade: isHttpsDowngrade,
|
||||
};
|
||||
}
|
||||
|
||||
if (!redirect_uri) {
|
||||
return {
|
||||
valid: isValid,
|
||||
@@ -39,10 +54,7 @@ export const useRedirectUri = (
|
||||
|
||||
isValid = true;
|
||||
|
||||
if (
|
||||
url.hostname == cookieDomain ||
|
||||
url.hostname.endsWith(`.${cookieDomain}`)
|
||||
) {
|
||||
if (isTrustedDomain(url, appUrlObj, cookieDomain, subdomainsEnabled)) {
|
||||
isTrusted = true;
|
||||
}
|
||||
|
||||
@@ -62,3 +74,45 @@ export const useRedirectUri = (
|
||||
httpsDowngrade: isHttpsDowngrade,
|
||||
};
|
||||
};
|
||||
|
||||
// ported from internal/controller/oauth_controller.go
|
||||
const getEffectivePort = (url: URL): string => {
|
||||
if (url.port) {
|
||||
return url.port;
|
||||
}
|
||||
|
||||
if (url.protocol == "https:") {
|
||||
return "443";
|
||||
}
|
||||
|
||||
return "80";
|
||||
};
|
||||
|
||||
export const isTrustedDomain = (
|
||||
url: URL,
|
||||
appUrl: URL,
|
||||
cookieDomain: string,
|
||||
subdomainsEnabled: boolean,
|
||||
): boolean => {
|
||||
if (url.protocol != appUrl.protocol) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (getEffectivePort(url) != getEffectivePort(appUrl)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (url.hostname == appUrl.hostname) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!subdomainsEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (url.hostname.endsWith("." + cookieDomain.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { z } from "zod";
|
||||
|
||||
type ScreenParams = {
|
||||
login_for?: "oidc" | "app";
|
||||
redirect_uri?: string;
|
||||
oidc_ticket?: string;
|
||||
oidc_scope?: string;
|
||||
oidc_name?: string;
|
||||
oidc_prompt?: "none" | "login";
|
||||
};
|
||||
|
||||
const zodScreenParams = z.object({
|
||||
login_for: z.enum(["oidc", "app"]).optional(),
|
||||
redirect_uri: z.string().optional(),
|
||||
oidc_ticket: z.string().optional(),
|
||||
oidc_scope: z.string().optional(),
|
||||
oidc_name: z.string().optional(),
|
||||
oidc_prompt: z.enum(["none", "login"]).optional(),
|
||||
});
|
||||
|
||||
export function useScreenParams(params: URLSearchParams): ScreenParams {
|
||||
const paramsObj = Object.fromEntries(params.entries());
|
||||
const parsed = zodScreenParams.safeParse(paramsObj);
|
||||
if (!parsed.success) {
|
||||
return {};
|
||||
}
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
export function recompileScreenParams(params: ScreenParams): string {
|
||||
const p = new URLSearchParams(
|
||||
Object.fromEntries(
|
||||
Object.entries(params).filter(([, v]) => v !== undefined),
|
||||
) as Record<string, string>,
|
||||
).toString();
|
||||
|
||||
if (p.length > 0) {
|
||||
return "?" + p;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Authorized",
|
||||
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
|
||||
"openidScopeName": "OpenID Connect",
|
||||
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||
"emailScopeName": "Email",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "Profile",
|
||||
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||
"groupsScopeName": "Groups",
|
||||
"groupsScopeDescription": "Allows the app to access your group information."
|
||||
"groupsScopeDescription": "Allows the app to access your group information.",
|
||||
"backToLoginButton": "Back to login",
|
||||
"phoneScopeName": "Phone",
|
||||
"phoneScopeDescription": "Allows the app to access your phone number.",
|
||||
"addressScopeName": "Address",
|
||||
"addressScopeDescription": "Allows the app to access your address.",
|
||||
"loginTailscaleTitle": "Continue with Tailscale",
|
||||
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
||||
"loginTailscaleDeviceName": "Device name:",
|
||||
"loginTailscaleOtherMethod": "Login with another method",
|
||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
|
||||
"quickActionsLanguage": "Language",
|
||||
"quickActionsTheme": "Theme",
|
||||
"quickActionsThemeLight": "Light",
|
||||
"quickActionsThemeDark": "Dark",
|
||||
"quickActionsThemeSystem": "System",
|
||||
"quickActionsLogout": "Logout",
|
||||
"quickActionsTitle": "Quick Actions",
|
||||
"quickActionsProviderLocal": "Local",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Authorized",
|
||||
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
|
||||
"openidScopeName": "OpenID Connect",
|
||||
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||
"emailScopeName": "Email",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "Profile",
|
||||
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||
"groupsScopeName": "Groups",
|
||||
"groupsScopeDescription": "Allows the app to access your group information."
|
||||
"groupsScopeDescription": "Allows the app to access your group information.",
|
||||
"backToLoginButton": "Back to login",
|
||||
"phoneScopeName": "Phone",
|
||||
"phoneScopeDescription": "Allows the app to access your phone number.",
|
||||
"addressScopeName": "Address",
|
||||
"addressScopeDescription": "Allows the app to access your address.",
|
||||
"loginTailscaleTitle": "Continue with Tailscale",
|
||||
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
||||
"loginTailscaleDeviceName": "Device name:",
|
||||
"loginTailscaleOtherMethod": "Login with another method",
|
||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
|
||||
"quickActionsLanguage": "Language",
|
||||
"quickActionsTheme": "Theme",
|
||||
"quickActionsThemeLight": "Light",
|
||||
"quickActionsThemeDark": "Dark",
|
||||
"quickActionsThemeSystem": "System",
|
||||
"quickActionsLogout": "Logout",
|
||||
"quickActionsTitle": "Quick Actions",
|
||||
"quickActionsProviderLocal": "Local",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Authorized",
|
||||
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
|
||||
"openidScopeName": "OpenID Connect",
|
||||
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||
"emailScopeName": "Email",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "Profile",
|
||||
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||
"groupsScopeName": "Groups",
|
||||
"groupsScopeDescription": "Allows the app to access your group information."
|
||||
"groupsScopeDescription": "Allows the app to access your group information.",
|
||||
"backToLoginButton": "Back to login",
|
||||
"phoneScopeName": "Phone",
|
||||
"phoneScopeDescription": "Allows the app to access your phone number.",
|
||||
"addressScopeName": "Address",
|
||||
"addressScopeDescription": "Allows the app to access your address.",
|
||||
"loginTailscaleTitle": "Continue with Tailscale",
|
||||
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
||||
"loginTailscaleDeviceName": "Device name:",
|
||||
"loginTailscaleOtherMethod": "Login with another method",
|
||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
|
||||
"quickActionsLanguage": "Language",
|
||||
"quickActionsTheme": "Theme",
|
||||
"quickActionsThemeLight": "Light",
|
||||
"quickActionsThemeDark": "Dark",
|
||||
"quickActionsThemeSystem": "System",
|
||||
"quickActionsLogout": "Logout",
|
||||
"quickActionsTitle": "Quick Actions",
|
||||
"quickActionsProviderLocal": "Local",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Authorized",
|
||||
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
|
||||
"openidScopeName": "OpenID Connect",
|
||||
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||
"emailScopeName": "Email",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "Profile",
|
||||
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||
"groupsScopeName": "Groups",
|
||||
"groupsScopeDescription": "Allows the app to access your group information."
|
||||
"groupsScopeDescription": "Allows the app to access your group information.",
|
||||
"backToLoginButton": "Back to login",
|
||||
"phoneScopeName": "Phone",
|
||||
"phoneScopeDescription": "Allows the app to access your phone number.",
|
||||
"addressScopeName": "Address",
|
||||
"addressScopeDescription": "Allows the app to access your address.",
|
||||
"loginTailscaleTitle": "Continue with Tailscale",
|
||||
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
||||
"loginTailscaleDeviceName": "Device name:",
|
||||
"loginTailscaleOtherMethod": "Login with another method",
|
||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
|
||||
"quickActionsLanguage": "Language",
|
||||
"quickActionsTheme": "Theme",
|
||||
"quickActionsThemeLight": "Light",
|
||||
"quickActionsThemeDark": "Dark",
|
||||
"quickActionsThemeSystem": "System",
|
||||
"quickActionsLogout": "Logout",
|
||||
"quickActionsTitle": "Quick Actions",
|
||||
"quickActionsProviderLocal": "Local",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Authorized",
|
||||
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
|
||||
"openidScopeName": "OpenID Connect",
|
||||
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||
"emailScopeName": "Email",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "Profile",
|
||||
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||
"groupsScopeName": "Groups",
|
||||
"groupsScopeDescription": "Allows the app to access your group information."
|
||||
"groupsScopeDescription": "Allows the app to access your group information.",
|
||||
"backToLoginButton": "Back to login",
|
||||
"phoneScopeName": "Phone",
|
||||
"phoneScopeDescription": "Allows the app to access your phone number.",
|
||||
"addressScopeName": "Address",
|
||||
"addressScopeDescription": "Allows the app to access your address.",
|
||||
"loginTailscaleTitle": "Continue with Tailscale",
|
||||
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
||||
"loginTailscaleDeviceName": "Device name:",
|
||||
"loginTailscaleOtherMethod": "Login with another method",
|
||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
|
||||
"quickActionsLanguage": "Language",
|
||||
"quickActionsTheme": "Theme",
|
||||
"quickActionsThemeLight": "Light",
|
||||
"quickActionsThemeDark": "Dark",
|
||||
"quickActionsThemeSystem": "System",
|
||||
"quickActionsLogout": "Logout",
|
||||
"quickActionsTitle": "Quick Actions",
|
||||
"quickActionsProviderLocal": "Local",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Authorized",
|
||||
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
|
||||
"openidScopeName": "OpenID Connect",
|
||||
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||
"emailScopeName": "Email",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "Profile",
|
||||
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||
"groupsScopeName": "Groups",
|
||||
"groupsScopeDescription": "Allows the app to access your group information."
|
||||
"groupsScopeDescription": "Ermöglicht der App, auf Ihre Benutzergruppen-Informationen zuzugreifen.",
|
||||
"backToLoginButton": "Zurück zum Login",
|
||||
"phoneScopeName": "Phone",
|
||||
"phoneScopeDescription": "Allows the app to access your phone number.",
|
||||
"addressScopeName": "Address",
|
||||
"addressScopeDescription": "Allows the app to access your address.",
|
||||
"loginTailscaleTitle": "Continue with Tailscale",
|
||||
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
||||
"loginTailscaleDeviceName": "Device name:",
|
||||
"loginTailscaleOtherMethod": "Login with another method",
|
||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
|
||||
"quickActionsLanguage": "Language",
|
||||
"quickActionsTheme": "Theme",
|
||||
"quickActionsThemeLight": "Light",
|
||||
"quickActionsThemeDark": "Dark",
|
||||
"quickActionsThemeSystem": "System",
|
||||
"quickActionsLogout": "Logout",
|
||||
"quickActionsTitle": "Quick Actions",
|
||||
"quickActionsProviderLocal": "Local",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"unauthorizedIpSubtitle": "Η διεύθυνση IP σας <code>{{ip}}</code> δεν είναι εξουσιοδοτημένη να έχει πρόσβαση στον πόρο <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Προσπαθήστε ξανά",
|
||||
"cancelTitle": "Ακύρωση",
|
||||
"forgotPasswordTitle": "Ξεχάσατε το συνθηματικό σας;",
|
||||
"forgotPasswordTitle": "Ξεχάσατε;",
|
||||
"failedToFetchProvidersTitle": "Αποτυχία φόρτωσης παρόχων πιστοποίησης. Παρακαλώ ελέγξτε τις ρυθμίσεις σας.",
|
||||
"errorTitle": "Παρουσιάστηκε ένα σφάλμα",
|
||||
"errorSubtitleInfo": "Το ακόλουθο σφάλμα προέκυψε κατά την επεξεργασία του αιτήματός σας:",
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Εξουσιοδοτημένος",
|
||||
"authorizeSuccessSubtitle": "Θα μεταφερθείτε στην εφαρμογή σε λίγα δευτερόλεπτα.",
|
||||
"authorizeErrorClientInfo": "Παρουσιάστηκε σφάλμα κατά τη φόρτωση των πληροφοριών. Παρακαλώ προσπαθήστε ξανά αργότερα.",
|
||||
"authorizeErrorMissingParams": "Οι παρακάτω απαραίτητες πληροφορίες λείπουν από το αίτημά σας: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "Το αίτημα δεν περιέχει τις απαιτούμενες παραμέτρους ή έχει μη έγκυρες παραμέτρους. Ελέγξτε τη διεύθυνση URL και προσπαθήστε ξανά.",
|
||||
"openidScopeName": "Σύνδεση OpenID",
|
||||
"openidScopeDescription": "Επιτρέπει στην εφαρμογή την πρόσβαση στις πληροφορίες σύνδεσης OpenID.",
|
||||
"emailScopeName": "Ηλεκτρονικό ταχυδρομείο",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "Προφίλ",
|
||||
"profileScopeDescription": "Επιτρέπει στην εφαρμογή να έχει πρόσβαση στις πληροφορίες του προφίλ σας.",
|
||||
"groupsScopeName": "Ομάδες",
|
||||
"groupsScopeDescription": "Επιτρέπει στην εφαρμογή την πρόσβαση στις πληροφορίες ομάδας σας."
|
||||
"groupsScopeDescription": "Επιτρέπει στην εφαρμογή την πρόσβαση στις πληροφορίες ομάδας σας.",
|
||||
"backToLoginButton": "Επιστροφή στη σελίδα εισόδου",
|
||||
"phoneScopeName": "Τηλέφωνο",
|
||||
"phoneScopeDescription": "Επιτρέπει στην εφαρμογή να έχει πρόσβαση στον αριθμό τηλεφώνου σας.",
|
||||
"addressScopeName": "Διεύθυνση",
|
||||
"addressScopeDescription": "Επιτρέπει στην εφαρμογή να έχει πρόσβαση στη διεύθυνσή σας.",
|
||||
"loginTailscaleTitle": "Συνέχεια με Tailscale",
|
||||
"loginTailscaleDescription": "Φαίνεται ότι έχετε πρόσβαση στο Tinyauth από μια εξουσιοδοτημένη συσκευή Tailscale. Θα θέλατε να συνεχίσετε με τη σύνδεση Tailscale σας;",
|
||||
"loginTailscaleDeviceName": "Όνομα συσκευής:",
|
||||
"loginTailscaleOtherMethod": "Σύνδεση με άλλη μέθοδο",
|
||||
"loginTailscaleSuccess": "Επιτυχής ταυτοποίηση με Tailscale.",
|
||||
"loginTailscaleFail": "Αποτυχία ταυτοποίησης με Tailscale. Δοκιμάστε ξανά ή χρησιμοποιήστε μια άλλη μέθοδο σύνδεσης.",
|
||||
"logoutTailscaleSubtitle": "Αυτή τη στιγμή είστε συνδεδεμένοι με Tailscale με τη συσκευή σας <code>{{deviceName}}</code>. Κάντε κλικ στο παρακάτω κουμπί για να αποσυνδεθείτε.",
|
||||
"quickActionsLanguage": "Γλώσσα",
|
||||
"quickActionsTheme": "Θέμα",
|
||||
"quickActionsThemeLight": "Φωτεινό",
|
||||
"quickActionsThemeDark": "Σκούρο",
|
||||
"quickActionsThemeSystem": "Σύστημα",
|
||||
"quickActionsLogout": "Αποσύνδεση",
|
||||
"quickActionsTitle": "Γρήγορες ενέργειες",
|
||||
"quickActionsProviderLocal": "Τοπικό",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Authorized",
|
||||
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
|
||||
"openidScopeName": "OpenID Connect",
|
||||
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||
"emailScopeName": "Email",
|
||||
@@ -88,9 +88,18 @@
|
||||
"loginTailscaleTitle": "Continue with Tailscale",
|
||||
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
||||
"loginTailscaleDeviceName": "Device name:",
|
||||
"loginTailscaleSubmit": "Continue with Tailscale",
|
||||
"loginTailscaleOtherMethod": "Login with another method",
|
||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout."
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
|
||||
"quickActionsLanguage": "Language",
|
||||
"quickActionsTheme": "Theme",
|
||||
"quickActionsThemeLight": "Light",
|
||||
"quickActionsThemeDark": "Dark",
|
||||
"quickActionsThemeSystem": "System",
|
||||
"quickActionsLogout": "Logout",
|
||||
"quickActionsTitle": "Quick Actions",
|
||||
"quickActionsProviderLocal": "Local",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Authorized",
|
||||
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
|
||||
"openidScopeName": "OpenID Connect",
|
||||
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||
"emailScopeName": "Email",
|
||||
@@ -88,9 +88,18 @@
|
||||
"loginTailscaleTitle": "Continue with Tailscale",
|
||||
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
||||
"loginTailscaleDeviceName": "Device name:",
|
||||
"loginTailscaleSubmit": "Continue with Tailscale",
|
||||
"loginTailscaleOtherMethod": "Login with another method",
|
||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout."
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
|
||||
"quickActionsLanguage": "Language",
|
||||
"quickActionsTheme": "Theme",
|
||||
"quickActionsThemeLight": "Light",
|
||||
"quickActionsThemeDark": "Dark",
|
||||
"quickActionsThemeSystem": "System",
|
||||
"quickActionsLogout": "Logout",
|
||||
"quickActionsTitle": "Quick Actions",
|
||||
"quickActionsProviderLocal": "Local",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Authorized",
|
||||
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
|
||||
"openidScopeName": "OpenID Connect",
|
||||
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||
"emailScopeName": "Email",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "Profile",
|
||||
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||
"groupsScopeName": "Groups",
|
||||
"groupsScopeDescription": "Allows the app to access your group information."
|
||||
"groupsScopeDescription": "Allows the app to access your group information.",
|
||||
"backToLoginButton": "Back to login",
|
||||
"phoneScopeName": "Phone",
|
||||
"phoneScopeDescription": "Allows the app to access your phone number.",
|
||||
"addressScopeName": "Address",
|
||||
"addressScopeDescription": "Allows the app to access your address.",
|
||||
"loginTailscaleTitle": "Continue with Tailscale",
|
||||
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
||||
"loginTailscaleDeviceName": "Device name:",
|
||||
"loginTailscaleOtherMethod": "Login with another method",
|
||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
|
||||
"quickActionsLanguage": "Language",
|
||||
"quickActionsTheme": "Theme",
|
||||
"quickActionsThemeLight": "Light",
|
||||
"quickActionsThemeDark": "Dark",
|
||||
"quickActionsThemeSystem": "System",
|
||||
"quickActionsLogout": "Logout",
|
||||
"quickActionsTitle": "Quick Actions",
|
||||
"quickActionsProviderLocal": "Local",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Authorized",
|
||||
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
|
||||
"openidScopeName": "OpenID Connect",
|
||||
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||
"emailScopeName": "Email",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "Profile",
|
||||
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||
"groupsScopeName": "Groups",
|
||||
"groupsScopeDescription": "Allows the app to access your group information."
|
||||
"groupsScopeDescription": "Allows the app to access your group information.",
|
||||
"backToLoginButton": "Back to login",
|
||||
"phoneScopeName": "Phone",
|
||||
"phoneScopeDescription": "Allows the app to access your phone number.",
|
||||
"addressScopeName": "Address",
|
||||
"addressScopeDescription": "Allows the app to access your address.",
|
||||
"loginTailscaleTitle": "Continue with Tailscale",
|
||||
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
||||
"loginTailscaleDeviceName": "Device name:",
|
||||
"loginTailscaleOtherMethod": "Login with another method",
|
||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
|
||||
"quickActionsLanguage": "Language",
|
||||
"quickActionsTheme": "Theme",
|
||||
"quickActionsThemeLight": "Light",
|
||||
"quickActionsThemeDark": "Dark",
|
||||
"quickActionsThemeSystem": "System",
|
||||
"quickActionsLogout": "Logout",
|
||||
"quickActionsTitle": "Quick Actions",
|
||||
"quickActionsProviderLocal": "Local",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Autorisé",
|
||||
"authorizeSuccessSubtitle": "Vous allez être redirigé vers l'application dans quelques secondes.",
|
||||
"authorizeErrorClientInfo": "Une erreur est survenue lors du chargement des informations du client. Veuillez réessayer plus tard.",
|
||||
"authorizeErrorMissingParams": "Les paramètres suivants sont manquants : {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
|
||||
"openidScopeName": "Connexion OpenID",
|
||||
"openidScopeDescription": "Autorise l'application à accéder à vos informations \"OpenID Connect\".",
|
||||
"emailScopeName": "Email",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "Profil",
|
||||
"profileScopeDescription": "Autorise l'application à accéder aux informations de votre profil.",
|
||||
"groupsScopeName": "Groupes",
|
||||
"groupsScopeDescription": "Autorise une application à accéder aux informations de votre groupe."
|
||||
"groupsScopeDescription": "Autorise une application à accéder aux informations de votre groupe.",
|
||||
"backToLoginButton": "Revenir à la page de connexion",
|
||||
"phoneScopeName": "Téléphone",
|
||||
"phoneScopeDescription": "Autorise l'application à accéder à votre numéro de téléphone.",
|
||||
"addressScopeName": "Adresse",
|
||||
"addressScopeDescription": "Autorise l'application d'accéder à votre adresse.",
|
||||
"loginTailscaleTitle": "Continuer avec Tailscale",
|
||||
"loginTailscaleDescription": "Vous semblez accéder à Tinyauth à partir d'un appareil Tailscale autorisé. Voulez-vous continuer avec votre connexion Tailscale ?",
|
||||
"loginTailscaleDeviceName": "Nom du périphérique:",
|
||||
"loginTailscaleOtherMethod": "Se connecter avec une autre méthode",
|
||||
"loginTailscaleSuccess": "Authentifié avec Tailscale.",
|
||||
"loginTailscaleFail": "Échec de l'authentification Tailscale. Veuillez réessayer ou utiliser une autre méthode de connexion.",
|
||||
"logoutTailscaleSubtitle": "Vous êtes actuellement connecté en tant que <code>{{deviceName}}</code>, cliquez sur le bouton ci-dessous pour vous déconnecter.",
|
||||
"quickActionsLanguage": "Language",
|
||||
"quickActionsTheme": "Theme",
|
||||
"quickActionsThemeLight": "Light",
|
||||
"quickActionsThemeDark": "Dark",
|
||||
"quickActionsThemeSystem": "System",
|
||||
"quickActionsLogout": "Logout",
|
||||
"quickActionsTitle": "Quick Actions",
|
||||
"quickActionsProviderLocal": "Local",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Authorized",
|
||||
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
|
||||
"openidScopeName": "OpenID Connect",
|
||||
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||
"emailScopeName": "Email",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "Profile",
|
||||
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||
"groupsScopeName": "Groups",
|
||||
"groupsScopeDescription": "Allows the app to access your group information."
|
||||
"groupsScopeDescription": "Allows the app to access your group information.",
|
||||
"backToLoginButton": "Back to login",
|
||||
"phoneScopeName": "Phone",
|
||||
"phoneScopeDescription": "Allows the app to access your phone number.",
|
||||
"addressScopeName": "Address",
|
||||
"addressScopeDescription": "Allows the app to access your address.",
|
||||
"loginTailscaleTitle": "Continue with Tailscale",
|
||||
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
||||
"loginTailscaleDeviceName": "Device name:",
|
||||
"loginTailscaleOtherMethod": "Login with another method",
|
||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
|
||||
"quickActionsLanguage": "Language",
|
||||
"quickActionsTheme": "Theme",
|
||||
"quickActionsThemeLight": "Light",
|
||||
"quickActionsThemeDark": "Dark",
|
||||
"quickActionsThemeSystem": "System",
|
||||
"quickActionsLogout": "Logout",
|
||||
"quickActionsTitle": "Quick Actions",
|
||||
"quickActionsProviderLocal": "Local",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Authorized",
|
||||
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
|
||||
"openidScopeName": "OpenID Connect",
|
||||
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||
"emailScopeName": "Email",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "Profile",
|
||||
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||
"groupsScopeName": "Groups",
|
||||
"groupsScopeDescription": "Allows the app to access your group information."
|
||||
"groupsScopeDescription": "Allows the app to access your group information.",
|
||||
"backToLoginButton": "Back to login",
|
||||
"phoneScopeName": "Phone",
|
||||
"phoneScopeDescription": "Allows the app to access your phone number.",
|
||||
"addressScopeName": "Address",
|
||||
"addressScopeDescription": "Allows the app to access your address.",
|
||||
"loginTailscaleTitle": "Continue with Tailscale",
|
||||
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
||||
"loginTailscaleDeviceName": "Device name:",
|
||||
"loginTailscaleOtherMethod": "Login with another method",
|
||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
|
||||
"quickActionsLanguage": "Language",
|
||||
"quickActionsTheme": "Theme",
|
||||
"quickActionsThemeLight": "Light",
|
||||
"quickActionsThemeDark": "Dark",
|
||||
"quickActionsThemeSystem": "System",
|
||||
"quickActionsLogout": "Logout",
|
||||
"quickActionsTitle": "Quick Actions",
|
||||
"quickActionsProviderLocal": "Local",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -58,8 +58,8 @@
|
||||
"invalidInput": "Input non valido",
|
||||
"domainWarningTitle": "Dominio non valido",
|
||||
"domainWarningSubtitle": "Stai accedendo a questa istanza da un dominio errato. Scegliendo di procedere, potresti incontrare problemi con l'autenticazione.",
|
||||
"domainWarningCurrent": "Current:",
|
||||
"domainWarningExpected": "Expected:",
|
||||
"domainWarningCurrent": "Attuale:",
|
||||
"domainWarningExpected": "Previsto:",
|
||||
"ignoreTitle": "Ignora",
|
||||
"goToCorrectDomainTitle": "Vai al dominio corretto",
|
||||
"authorizeTitle": "Autorizza",
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Autorizzato",
|
||||
"authorizeSuccessSubtitle": "Verrai reindirizzato all'app in pochi secondi.",
|
||||
"authorizeErrorClientInfo": "Si è verificato un errore durante il caricamento delle informazioni del client. Riprova.",
|
||||
"authorizeErrorMissingParams": "I seguenti parametri sono mancanti: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
|
||||
"openidScopeName": "OpenID Connect",
|
||||
"openidScopeDescription": "Permetti all'app di accedere alle tue informazioni OpenID Connect.",
|
||||
"emailScopeName": "Email",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "Profilo",
|
||||
"profileScopeDescription": "Consenti all'app di accedere alle informazioni del tuo profilo.",
|
||||
"groupsScopeName": "Gruppi",
|
||||
"groupsScopeDescription": "Consenti all'app di accedere alle informazioni sui tuoi gruppi."
|
||||
"groupsScopeDescription": "Consenti all'app di accedere alle informazioni sui tuoi gruppi.",
|
||||
"backToLoginButton": "Back to login",
|
||||
"phoneScopeName": "Phone",
|
||||
"phoneScopeDescription": "Allows the app to access your phone number.",
|
||||
"addressScopeName": "Address",
|
||||
"addressScopeDescription": "Allows the app to access your address.",
|
||||
"loginTailscaleTitle": "Continue with Tailscale",
|
||||
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
||||
"loginTailscaleDeviceName": "Device name:",
|
||||
"loginTailscaleOtherMethod": "Login with another method",
|
||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
|
||||
"quickActionsLanguage": "Language",
|
||||
"quickActionsTheme": "Theme",
|
||||
"quickActionsThemeLight": "Light",
|
||||
"quickActionsThemeDark": "Dark",
|
||||
"quickActionsThemeSystem": "System",
|
||||
"quickActionsLogout": "Logout",
|
||||
"quickActionsTitle": "Quick Actions",
|
||||
"quickActionsProviderLocal": "Local",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -2,44 +2,44 @@
|
||||
"loginTitle": "Welcome back, login with",
|
||||
"loginTitleSimple": "Welcome back, please login",
|
||||
"loginDivider": "Or",
|
||||
"loginUsername": "Username",
|
||||
"loginPassword": "Password",
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginUsername": "ユーザーネーム",
|
||||
"loginPassword": "パスワード",
|
||||
"loginSubmit": "ログイン",
|
||||
"loginFailTitle": "ログイン出来ない",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times. Please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessTitle": "ログイン済み",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "An error occurred",
|
||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
||||
"loginOauthSuccessTitle": "Redirecting",
|
||||
"loginOauthSuccessTitle": "リダイレクトしています",
|
||||
"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...",
|
||||
"loginOauthAutoRedirectButton": "今すぐリダイレクト",
|
||||
"continueTitle": "続行",
|
||||
"continueRedirectingTitle": "リダイレクトしています…",
|
||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
||||
"continueRedirectManually": "Redirect me manually",
|
||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
||||
"continueInsecureRedirectTitle": "安全でないリダイレク",
|
||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
|
||||
"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": "Failed to log out",
|
||||
"logoutFailTitle": "ログアウト出来ません",
|
||||
"logoutFailSubtitle": "Please try again",
|
||||
"logoutSuccessTitle": "Logged out",
|
||||
"logoutSuccessSubtitle": "You have been logged out",
|
||||
"logoutTitle": "Logout",
|
||||
"logoutSuccessSubtitle": "ログアウトしました",
|
||||
"logoutTitle": "ログアウト",
|
||||
"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",
|
||||
"notFoundTitle": "ページが見つかりません",
|
||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
||||
"notFoundButton": "Go home",
|
||||
"notFoundButton": "ホームに戻る",
|
||||
"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",
|
||||
"totpSuccessSubtitle": "アプリにリダイレクトしています",
|
||||
"totpTitle": "TOTPコード入れて",
|
||||
"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>.",
|
||||
@@ -47,8 +47,8 @@
|
||||
"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",
|
||||
"cancelTitle": "Cancel",
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"cancelTitle": "キャンセル",
|
||||
"forgotPasswordTitle": "パスワード忘れた?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||
@@ -71,13 +71,35 @@
|
||||
"authorizeSuccessTitle": "Authorized",
|
||||
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
|
||||
"openidScopeName": "OpenID Connect",
|
||||
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||
"emailScopeName": "Email",
|
||||
"emailScopeName": "Eメール",
|
||||
"emailScopeDescription": "Allows the app to access your email address.",
|
||||
"profileScopeName": "Profile",
|
||||
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||
"groupsScopeName": "Groups",
|
||||
"groupsScopeDescription": "Allows the app to access your group information."
|
||||
"groupsScopeDescription": "Allows the app to access your group information.",
|
||||
"backToLoginButton": "Back to login",
|
||||
"phoneScopeName": "Phone",
|
||||
"phoneScopeDescription": "Allows the app to access your phone number.",
|
||||
"addressScopeName": "Address",
|
||||
"addressScopeDescription": "Allows the app to access your address.",
|
||||
"loginTailscaleTitle": "Continue with Tailscale",
|
||||
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
||||
"loginTailscaleDeviceName": "Device name:",
|
||||
"loginTailscaleOtherMethod": "Login with another method",
|
||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
|
||||
"quickActionsLanguage": "Language",
|
||||
"quickActionsTheme": "Theme",
|
||||
"quickActionsThemeLight": "Light",
|
||||
"quickActionsThemeDark": "Dark",
|
||||
"quickActionsThemeSystem": "System",
|
||||
"quickActionsLogout": "Logout",
|
||||
"quickActionsTitle": "Quick Actions",
|
||||
"quickActionsProviderLocal": "Local",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "권한 부여 완료",
|
||||
"authorizeSuccessSubtitle": "몇 초 후에 앱으로 리디렉션됩니다.",
|
||||
"authorizeErrorClientInfo": "클라이언트 정보를 불러오는 중 오류가 발생했습니다. 나중에 다시 시도해 주세요.",
|
||||
"authorizeErrorMissingParams": "다음 매개변수가 누락되었습니다: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "요청에 필수 매개변수가 누락되었거나 유효하지 않은 매개변수가 포함되어 있습니다. URL을 확인한 후 다시 시도해 주세요.",
|
||||
"openidScopeName": "OpenID Connect",
|
||||
"openidScopeDescription": "앱이 OpenID Connect 정보에 접근할 수 있도록 허용합니다.",
|
||||
"emailScopeName": "이메일",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "프로필",
|
||||
"profileScopeDescription": "앱이 프로필 정보에 접근할 수 있도록 허용합니다.",
|
||||
"groupsScopeName": "그룹",
|
||||
"groupsScopeDescription": "앱이 그룹 정보에 접근할 수 있도록 허용합니다."
|
||||
"groupsScopeDescription": "앱이 그룹 정보에 접근할 수 있도록 허용합니다.",
|
||||
"backToLoginButton": "로그인 페이지로 돌아가기",
|
||||
"phoneScopeName": "전화번호",
|
||||
"phoneScopeDescription": "앱이 전화번호에 접근할 수 있도록 허용합니다.",
|
||||
"addressScopeName": "주소",
|
||||
"addressScopeDescription": "앱이 주소에 접근할 수 있도록 허용합니다.",
|
||||
"loginTailscaleTitle": "Tailscale로 계속",
|
||||
"loginTailscaleDescription": "사용자님은 인증된 Tailscale 기기에서 Tinyauth에 접속 중인 것으로 보입니다. Tailscale 연결을 계속하시겠습니까?",
|
||||
"loginTailscaleDeviceName": "기기 이름:",
|
||||
"loginTailscaleOtherMethod": "다른 방법으로 로그인",
|
||||
"loginTailscaleSuccess": "Tailscale을 통해 성공적으로 인증되었습니다.",
|
||||
"loginTailscaleFail": "Tailscale을 통한 인증에 실패했습니다. 다른 방식을 사용해 주세요.",
|
||||
"logoutTailscaleSubtitle": "현재 <code>{{deviceName}}</code>가 Tailscale을 통해 로그인 되어 있습니다. 로그아웃 하시려면 아래 버튼을 클릭하세요.",
|
||||
"quickActionsLanguage": "언어",
|
||||
"quickActionsTheme": "테마",
|
||||
"quickActionsThemeLight": "밝은 테마",
|
||||
"quickActionsThemeDark": "어두운 테마",
|
||||
"quickActionsThemeSystem": "시스템",
|
||||
"quickActionsLogout": "로그아웃",
|
||||
"quickActionsTitle": "빠른 동작",
|
||||
"quickActionsProviderLocal": "로컬",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Geautoriseerd",
|
||||
"authorizeSuccessSubtitle": "Je wordt binnen enkele seconden doorgestuurd naar de app.",
|
||||
"authorizeErrorClientInfo": "Er is een fout opgetreden tijdens het laden van de cliëntinformatie. Probeer het later opnieuw.",
|
||||
"authorizeErrorMissingParams": "De volgende parameters ontbreken: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "Er ontbreken verplichte parameters voor het verzoek of er zijn ongeldige parameters. Controleer de URL en probeer het opnieuw.",
|
||||
"openidScopeName": "OpenID Connect",
|
||||
"openidScopeDescription": "Hiermee kan de app toegang krijgen tot jouw OpenID Connect-informatie.",
|
||||
"emailScopeName": "E-mail",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "Profiel",
|
||||
"profileScopeDescription": "Hiermee kan de app toegang krijgen tot je profielinformatie.",
|
||||
"groupsScopeName": "Groepen",
|
||||
"groupsScopeDescription": "Hiermee kan de app toegang krijgen tot jouw groepsinformatie."
|
||||
"groupsScopeDescription": "Hiermee kan de app toegang krijgen tot jouw groepsinformatie.",
|
||||
"backToLoginButton": "Terug naar Inloggen",
|
||||
"phoneScopeName": "Telefoon",
|
||||
"phoneScopeDescription": "Geeft de app toegang tot je telefoonnummer.",
|
||||
"addressScopeName": "Adres",
|
||||
"addressScopeDescription": "Geeft de app toegang tot je adres.",
|
||||
"loginTailscaleTitle": "Doorgaan met Tailscale",
|
||||
"loginTailscaleDescription": "Je lijkt toegang te hebben tot Tinyauth vanaf een geautoriseerd Tailscale-apparaat. Wil je doorgaan met je Tailscale-verbinding?",
|
||||
"loginTailscaleDeviceName": "Apparaatnaam:",
|
||||
"loginTailscaleOtherMethod": "Op een andere manier inloggen",
|
||||
"loginTailscaleSuccess": "Succesvol geauthenticeerd met Tailscale.",
|
||||
"loginTailscaleFail": "Authenticatie met Tailscale mislukt. Probeer het opnieuw of gebruik een andere inlogmethode.",
|
||||
"logoutTailscaleSubtitle": "Je bent momenteel ingelogd met Tailscale op je apparaat <code>{{deviceName}}</code>. Klik op de knop hieronder om uit te loggen.",
|
||||
"quickActionsLanguage": "Taal",
|
||||
"quickActionsTheme": "Thema",
|
||||
"quickActionsThemeLight": "Licht",
|
||||
"quickActionsThemeDark": "Donker",
|
||||
"quickActionsThemeSystem": "Systeem",
|
||||
"quickActionsLogout": "Uitloggen",
|
||||
"quickActionsTitle": "Snelle acties",
|
||||
"quickActionsProviderLocal": "Lokaal",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Authorized",
|
||||
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
|
||||
"openidScopeName": "OpenID Connect",
|
||||
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||
"emailScopeName": "Email",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "Profile",
|
||||
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||
"groupsScopeName": "Groups",
|
||||
"groupsScopeDescription": "Allows the app to access your group information."
|
||||
"groupsScopeDescription": "Allows the app to access your group information.",
|
||||
"backToLoginButton": "Back to login",
|
||||
"phoneScopeName": "Phone",
|
||||
"phoneScopeDescription": "Allows the app to access your phone number.",
|
||||
"addressScopeName": "Address",
|
||||
"addressScopeDescription": "Allows the app to access your address.",
|
||||
"loginTailscaleTitle": "Continue with Tailscale",
|
||||
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
||||
"loginTailscaleDeviceName": "Device name:",
|
||||
"loginTailscaleOtherMethod": "Login with another method",
|
||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
|
||||
"quickActionsLanguage": "Language",
|
||||
"quickActionsTheme": "Theme",
|
||||
"quickActionsThemeLight": "Light",
|
||||
"quickActionsThemeDark": "Dark",
|
||||
"quickActionsThemeSystem": "System",
|
||||
"quickActionsLogout": "Logout",
|
||||
"quickActionsTitle": "Quick Actions",
|
||||
"quickActionsProviderLocal": "Local",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -71,13 +71,35 @@
|
||||
"authorizeSuccessTitle": "Autoryzowano",
|
||||
"authorizeSuccessSubtitle": "Za kilka sekund nastąpi przekierowanie do aplikacji.",
|
||||
"authorizeErrorClientInfo": "Wystąpił błąd podczas ładowania informacji o kliencie. Spróbuj ponownie później.",
|
||||
"authorizeErrorMissingParams": "Brakuje następujących parametrów: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "Żądanie nie zawiera wymaganych parametrów lub zawiera nieprawidłowe parametry. Sprawdź adres URL i spróbuj ponownie.",
|
||||
"openidScopeName": "OpenID Connect",
|
||||
"openidScopeDescription": "Zezwala aplikacji na dostęp do informacji o OpenID Connect.",
|
||||
"emailScopeName": "E-mail",
|
||||
"emailScopeDescription": "Zezwala aplikacji na dostęp do adresów e-mail.",
|
||||
"profileScopeName": "Profil",
|
||||
"profileScopeDescription": "Zezwala aplikacji na dostęp do informacji o porfilu.",
|
||||
"profileScopeDescription": "Zezwala aplikacji na dostęp do informacji o profilu.",
|
||||
"groupsScopeName": "Grupy",
|
||||
"groupsScopeDescription": "Zezwala aplikacji na dostęp do informacji o grupie."
|
||||
"groupsScopeDescription": "Zezwala aplikacji na dostęp do informacji o grupie.",
|
||||
"backToLoginButton": "Wróć do logowania",
|
||||
"phoneScopeName": "Telefon",
|
||||
"phoneScopeDescription": "Pozwala aplikacji na dostęp do Twojego numeru telefonu.",
|
||||
"addressScopeName": "Adres",
|
||||
"addressScopeDescription": "Pozwala aplikacji na dostęp do Twojego adresu.",
|
||||
"loginTailscaleTitle": "Kontynuuj poprzez Tailscale",
|
||||
"loginTailscaleDescription": "Wygląda na to, że uzyskujesz dostęp do Tinyauth z autoryzowanego urządzenia Tailscale. Czy chcesz kontynuować połączenie poprzez Tailscale?",
|
||||
"loginTailscaleDeviceName": "Nazwa urządzenia:",
|
||||
"loginTailscaleOtherMethod": "Zaloguj się inną metodą",
|
||||
"loginTailscaleSuccess": "Pomyślnie uwierzytelniono poprzez Tailscale.",
|
||||
"loginTailscaleFail": "Nie udało się uwierzytelnić poprzez Tailscale. Spróbuj ponownie lub użyj innej metody logowania.",
|
||||
"logoutTailscaleSubtitle": "Jesteś obecnie zalogowany jako <code>{{deviceName}}</code>. Kliknij poniższy przycisk, aby się wylogować.",
|
||||
"quickActionsLanguage": "Język",
|
||||
"quickActionsTheme": "Motyw",
|
||||
"quickActionsThemeLight": "Jasny",
|
||||
"quickActionsThemeDark": "Ciemny",
|
||||
"quickActionsThemeSystem": "System",
|
||||
"quickActionsLogout": "Wyloguj się",
|
||||
"quickActionsTitle": "Szybkie czynności",
|
||||
"quickActionsProviderLocal": "Lokalny",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Authorized",
|
||||
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
|
||||
"openidScopeName": "OpenID Connect",
|
||||
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||
"emailScopeName": "Email",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "Profile",
|
||||
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||
"groupsScopeName": "Groups",
|
||||
"groupsScopeDescription": "Allows the app to access your group information."
|
||||
"groupsScopeDescription": "Allows the app to access your group information.",
|
||||
"backToLoginButton": "Back to login",
|
||||
"phoneScopeName": "Phone",
|
||||
"phoneScopeDescription": "Allows the app to access your phone number.",
|
||||
"addressScopeName": "Address",
|
||||
"addressScopeDescription": "Allows the app to access your address.",
|
||||
"loginTailscaleTitle": "Continue with Tailscale",
|
||||
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
||||
"loginTailscaleDeviceName": "Device name:",
|
||||
"loginTailscaleOtherMethod": "Login with another method",
|
||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
|
||||
"quickActionsLanguage": "Language",
|
||||
"quickActionsTheme": "Theme",
|
||||
"quickActionsThemeLight": "Light",
|
||||
"quickActionsThemeDark": "Dark",
|
||||
"quickActionsThemeSystem": "System",
|
||||
"quickActionsLogout": "Logout",
|
||||
"quickActionsTitle": "Quick Actions",
|
||||
"quickActionsProviderLocal": "Local",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Authorized",
|
||||
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
|
||||
"openidScopeName": "OpenID Connect",
|
||||
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||
"emailScopeName": "Email",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "Profile",
|
||||
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||
"groupsScopeName": "Groups",
|
||||
"groupsScopeDescription": "Allows the app to access your group information."
|
||||
"groupsScopeDescription": "Allows the app to access your group information.",
|
||||
"backToLoginButton": "Back to login",
|
||||
"phoneScopeName": "Phone",
|
||||
"phoneScopeDescription": "Allows the app to access your phone number.",
|
||||
"addressScopeName": "Address",
|
||||
"addressScopeDescription": "Allows the app to access your address.",
|
||||
"loginTailscaleTitle": "Continue with Tailscale",
|
||||
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
||||
"loginTailscaleDeviceName": "Device name:",
|
||||
"loginTailscaleOtherMethod": "Login with another method",
|
||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
|
||||
"quickActionsLanguage": "Language",
|
||||
"quickActionsTheme": "Theme",
|
||||
"quickActionsThemeLight": "Light",
|
||||
"quickActionsThemeDark": "Dark",
|
||||
"quickActionsThemeSystem": "System",
|
||||
"quickActionsLogout": "Logout",
|
||||
"quickActionsTitle": "Quick Actions",
|
||||
"quickActionsProviderLocal": "Local",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Authorized",
|
||||
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
|
||||
"openidScopeName": "OpenID Connect",
|
||||
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||
"emailScopeName": "Email",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "Profile",
|
||||
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||
"groupsScopeName": "Groups",
|
||||
"groupsScopeDescription": "Allows the app to access your group information."
|
||||
"groupsScopeDescription": "Allows the app to access your group information.",
|
||||
"backToLoginButton": "Back to login",
|
||||
"phoneScopeName": "Phone",
|
||||
"phoneScopeDescription": "Allows the app to access your phone number.",
|
||||
"addressScopeName": "Address",
|
||||
"addressScopeDescription": "Allows the app to access your address.",
|
||||
"loginTailscaleTitle": "Continue with Tailscale",
|
||||
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
||||
"loginTailscaleDeviceName": "Device name:",
|
||||
"loginTailscaleOtherMethod": "Login with another method",
|
||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
|
||||
"quickActionsLanguage": "Language",
|
||||
"quickActionsTheme": "Theme",
|
||||
"quickActionsThemeLight": "Light",
|
||||
"quickActionsThemeDark": "Dark",
|
||||
"quickActionsThemeSystem": "System",
|
||||
"quickActionsLogout": "Logout",
|
||||
"quickActionsTitle": "Quick Actions",
|
||||
"quickActionsProviderLocal": "Local",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"fieldRequired": "Это поле является обязательным",
|
||||
"invalidInput": "Недопустимый ввод",
|
||||
"domainWarningTitle": "Неверный домен",
|
||||
"domainWarningSubtitle": "You are accessing this instance from an incorrect domain. If you proceed, you may encounter issues with authentication.",
|
||||
"domainWarningSubtitle": "Вы получаете доступ к этому экземпляру приложения через некорректный домен. Если вы продолжите, вы можете столкнуться с проблемами аутентификации.",
|
||||
"domainWarningCurrent": "Текущий:",
|
||||
"domainWarningExpected": "Ожидается:",
|
||||
"ignoreTitle": "Игнорировать",
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Разрешено",
|
||||
"authorizeSuccessSubtitle": "Вы будете перенаправлены в приложение через несколько секунд.",
|
||||
"authorizeErrorClientInfo": "Произошла ошибка при загрузке информации о клиенте. Пожалуйста, повторите попытку позже.",
|
||||
"authorizeErrorMissingParams": "Отсутствуют следующие параметры: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "В запросе отсутствуют необходимые параметры или указаны недопустимые параметры. Проверьте URL-адрес и попробуйте снова.",
|
||||
"openidScopeName": "Подключение OpenID",
|
||||
"openidScopeDescription": "Приложение сможет получить доступ к информации подключённого OpenID.",
|
||||
"emailScopeName": "Эл. Почта",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "Профиль",
|
||||
"profileScopeDescription": "Приложение сможет получить доступ к информации вашего профиля.",
|
||||
"groupsScopeName": "Группы",
|
||||
"groupsScopeDescription": "Приложение сможет получать доступ к информации о вашей группе."
|
||||
"groupsScopeDescription": "Приложение сможет получать доступ к информации о вашей группе.",
|
||||
"backToLoginButton": "Вернуться к форме входа",
|
||||
"phoneScopeName": "Телефон",
|
||||
"phoneScopeDescription": "Приложение сможет получить доступ к вашему номеру телефона.",
|
||||
"addressScopeName": "Адрес",
|
||||
"addressScopeDescription": "Приложение сможет получить доступ к вашему адресу.",
|
||||
"loginTailscaleTitle": "Продолжить через Tailscale",
|
||||
"loginTailscaleDescription": "Похоже, вы используете авторизованное устройство Tailscale для доступа к Tinyauth. Хотите продолжить через подключение Tailscale?",
|
||||
"loginTailscaleDeviceName": "Имя устройства:",
|
||||
"loginTailscaleOtherMethod": "Войти другим способом",
|
||||
"loginTailscaleSuccess": "Успешная авторизация через Tailscale.",
|
||||
"loginTailscaleFail": "Не удалось авторизоваться через Tailscale. Пожалуйста, попробуйте ещё раз, или используйте другой способ.",
|
||||
"logoutTailscaleSubtitle": "Вы вошли через Tailscale на устройстве <code>{{deviceName}}</code>. Нажмите кнопку ниже, чтобы выйти.",
|
||||
"quickActionsLanguage": "Язык",
|
||||
"quickActionsTheme": "Тема",
|
||||
"quickActionsThemeLight": "Светлая",
|
||||
"quickActionsThemeDark": "Тёмная",
|
||||
"quickActionsThemeSystem": "Система",
|
||||
"quickActionsLogout": "Выйти",
|
||||
"quickActionsTitle": "Быстрые действия",
|
||||
"quickActionsProviderLocal": "Локальный",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"fieldRequired": "Ово поље је неопходно",
|
||||
"invalidInput": "Неисправан унос",
|
||||
"domainWarningTitle": "Неисправан домен",
|
||||
"domainWarningSubtitle": "You are accessing this instance from an incorrect domain. If you proceed, you may encounter issues with authentication.",
|
||||
"domainWarningSubtitle": "Приступате овој инстанци са неисправног домена. Ако наставите, можете наићи на проблеме са аутентификацијом.",
|
||||
"domainWarningCurrent": "Тренутни:",
|
||||
"domainWarningExpected": "Очекивани:",
|
||||
"ignoreTitle": "Игнориши",
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Ауторизован",
|
||||
"authorizeSuccessSubtitle": "Бићете преусмерени на апликацију за неколико секунди.",
|
||||
"authorizeErrorClientInfo": "Појавила се грешка током учитавања информација о клијенту. Молим вас покушајте поново касније.",
|
||||
"authorizeErrorMissingParams": "Следећи параметри недостају: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "Захтеву недостају неопходни параметри или су они неисправни. Молим вас проверите адресу и покушајте поново.",
|
||||
"openidScopeName": "OpenID повезивање",
|
||||
"openidScopeDescription": "Омогућава апликацији да приступа информацији о вашој OpenID вези.",
|
||||
"emailScopeName": "Е-пошта",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "Профил",
|
||||
"profileScopeDescription": "Омогућава апликацији да приступа информацијама о вашем профилу.",
|
||||
"groupsScopeName": "Групе",
|
||||
"groupsScopeDescription": "Омогућава апликацији да приступа информацијама о вашој групи."
|
||||
"groupsScopeDescription": "Омогућава апликацији да приступа информацијама о вашој групи.",
|
||||
"backToLoginButton": "Назад на пријаву",
|
||||
"phoneScopeName": "Телефон",
|
||||
"phoneScopeDescription": "Дозволите овој апликацији да приступи вашем броју телефона.",
|
||||
"addressScopeName": "Адреса",
|
||||
"addressScopeDescription": "Омогућава апликацији да приступи вашој адреси.",
|
||||
"loginTailscaleTitle": "Настави са Tailscale-ом",
|
||||
"loginTailscaleDescription": "Изгледа да приступате Tinyauth-у са овлашћеног Tailscale уређаја. Да ли желите да наставите са Tailscale везом?",
|
||||
"loginTailscaleDeviceName": "Назив уређаја:",
|
||||
"loginTailscaleOtherMethod": "Пријави се на други начин",
|
||||
"loginTailscaleSuccess": "Успешно аутентификован са Tailscale-ом.",
|
||||
"loginTailscaleFail": "Неуспела аутентификација са Tailscale-ом. Молим вас покушајте поново или користите други начин пријаве.",
|
||||
"logoutTailscaleSubtitle": "Тренутно сте пријављени са Tailscale-ом на вашем уређају <code>{{deviceName}}</code>. Кликните на дугме испод за одјаву.",
|
||||
"quickActionsLanguage": "Језик",
|
||||
"quickActionsTheme": "Тема",
|
||||
"quickActionsThemeLight": "Светло",
|
||||
"quickActionsThemeDark": "Тамна",
|
||||
"quickActionsThemeSystem": "Систем",
|
||||
"quickActionsLogout": "Одјава",
|
||||
"quickActionsTitle": "Брзе радње",
|
||||
"quickActionsProviderLocal": "Локално",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Authorized",
|
||||
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
|
||||
"openidScopeName": "OpenID Connect",
|
||||
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||
"emailScopeName": "Email",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "Profile",
|
||||
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||
"groupsScopeName": "Groups",
|
||||
"groupsScopeDescription": "Allows the app to access your group information."
|
||||
"groupsScopeDescription": "Allows the app to access your group information.",
|
||||
"backToLoginButton": "Back to login",
|
||||
"phoneScopeName": "Phone",
|
||||
"phoneScopeDescription": "Allows the app to access your phone number.",
|
||||
"addressScopeName": "Address",
|
||||
"addressScopeDescription": "Allows the app to access your address.",
|
||||
"loginTailscaleTitle": "Continue with Tailscale",
|
||||
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
||||
"loginTailscaleDeviceName": "Device name:",
|
||||
"loginTailscaleOtherMethod": "Login with another method",
|
||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
|
||||
"quickActionsLanguage": "Language",
|
||||
"quickActionsTheme": "Theme",
|
||||
"quickActionsThemeLight": "Light",
|
||||
"quickActionsThemeDark": "Dark",
|
||||
"quickActionsThemeSystem": "System",
|
||||
"quickActionsLogout": "Logout",
|
||||
"quickActionsTitle": "Quick Actions",
|
||||
"quickActionsProviderLocal": "Local",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Authorized",
|
||||
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
|
||||
"openidScopeName": "OpenID Connect",
|
||||
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||
"emailScopeName": "Email",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "Profile",
|
||||
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||
"groupsScopeName": "Groups",
|
||||
"groupsScopeDescription": "Allows the app to access your group information."
|
||||
"groupsScopeDescription": "Allows the app to access your group information.",
|
||||
"backToLoginButton": "Back to login",
|
||||
"phoneScopeName": "Phone",
|
||||
"phoneScopeDescription": "Allows the app to access your phone number.",
|
||||
"addressScopeName": "Address",
|
||||
"addressScopeDescription": "Allows the app to access your address.",
|
||||
"loginTailscaleTitle": "Continue with Tailscale",
|
||||
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
||||
"loginTailscaleDeviceName": "Device name:",
|
||||
"loginTailscaleOtherMethod": "Login with another method",
|
||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
|
||||
"quickActionsLanguage": "Language",
|
||||
"quickActionsTheme": "Theme",
|
||||
"quickActionsThemeLight": "Light",
|
||||
"quickActionsThemeDark": "Dark",
|
||||
"quickActionsThemeSystem": "System",
|
||||
"quickActionsLogout": "Logout",
|
||||
"quickActionsTitle": "Quick Actions",
|
||||
"quickActionsProviderLocal": "Local",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"logoutSuccessTitle": "Ви вийшли",
|
||||
"logoutSuccessSubtitle": "Ви вийшли з системи",
|
||||
"logoutTitle": "Вийти",
|
||||
"logoutUsernameSubtitle": "Зараз ви увійшли як <code>{{username}}</code>. Натисніть кнопку нижче для виходу.",
|
||||
"logoutUsernameSubtitle": "Ви увійшли як <code>{{username}}</code>. Натисніть кнопку нижче для виходу.",
|
||||
"logoutOauthSubtitle": "Наразі ви увійшли як <code>{{username}}</code> використовуючи провайдера {{provider}} OAuth. Натисніть кнопку нижче, щоб вийти.",
|
||||
"notFoundTitle": "Сторінку не знайдено",
|
||||
"notFoundSubtitle": "Сторінка, яку ви шукаєте, не існує.",
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Авторизовано",
|
||||
"authorizeSuccessSubtitle": "Вас буде перенаправлено до програми за декілька секунд.",
|
||||
"authorizeErrorClientInfo": "Під час завантаження даних клієнта сталася помилка. Будь ласка, спробуйте ще раз пізніше.",
|
||||
"authorizeErrorMissingParams": "Відсутні наступні параметри: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "У запиті відсутні обов'язкові або містяться некоректні параметри. Будь ласка, перевірте URL-адресу та спробуйте знову.",
|
||||
"openidScopeName": "OpenID Connect",
|
||||
"openidScopeDescription": "Дозволяє програмі отримувати доступ до вашої інформації OpenID Connect.",
|
||||
"emailScopeName": "Електронна пошта",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "Профіль",
|
||||
"profileScopeDescription": "Дозволяє програмі отримувати доступ до інформації вашого профілю.",
|
||||
"groupsScopeName": "Групи",
|
||||
"groupsScopeDescription": "Дозволяє програмі отримувати доступ до інформації про групу."
|
||||
"groupsScopeDescription": "Дозволяє програмі отримувати доступ до інформації про групу.",
|
||||
"backToLoginButton": "Повернутися на сторінку входу",
|
||||
"phoneScopeName": "Номер телефону",
|
||||
"phoneScopeDescription": "Дозволяє програмі отримувати доступ до вашого номера телефону.",
|
||||
"addressScopeName": "Адреса",
|
||||
"addressScopeDescription": "Дозволяє програмі отримувати доступ до вашої адреси.",
|
||||
"loginTailscaleTitle": "Продовжити з Tailscale",
|
||||
"loginTailscaleDescription": "Здається, Ви отримали доступ до Tinyauth з авторизованого пристрою Tailscale. Продовжити з'єднання з Tailscale?",
|
||||
"loginTailscaleDeviceName": "Назва пристрою:",
|
||||
"loginTailscaleOtherMethod": "Увійти за допомогою іншого методу",
|
||||
"loginTailscaleSuccess": "Успішно авторизовано з Tailscale.",
|
||||
"loginTailscaleFail": "Не вдалося увійти з Tailscale. Будь ласка, спробуйте ще раз або скористайтеся іншим методом авторизації.",
|
||||
"logoutTailscaleSubtitle": "Наразі ви ввійшли за допомогою Tailscale на пристрої <code>{{deviceName}}</code>. Натисніть на кнопку нижче, щоб вийти.",
|
||||
"quickActionsLanguage": "Мова",
|
||||
"quickActionsTheme": "Тема",
|
||||
"quickActionsThemeLight": "Світла",
|
||||
"quickActionsThemeDark": "Темна",
|
||||
"quickActionsThemeSystem": "Система",
|
||||
"quickActionsLogout": "Вийти",
|
||||
"quickActionsTitle": "Швидкі дії",
|
||||
"quickActionsProviderLocal": "Local",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "Authorized",
|
||||
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
|
||||
"openidScopeName": "OpenID Connect",
|
||||
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||
"emailScopeName": "Email",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "Profile",
|
||||
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||
"groupsScopeName": "Groups",
|
||||
"groupsScopeDescription": "Allows the app to access your group information."
|
||||
"groupsScopeDescription": "Allows the app to access your group information.",
|
||||
"backToLoginButton": "Back to login",
|
||||
"phoneScopeName": "Phone",
|
||||
"phoneScopeDescription": "Allows the app to access your phone number.",
|
||||
"addressScopeName": "Address",
|
||||
"addressScopeDescription": "Allows the app to access your address.",
|
||||
"loginTailscaleTitle": "Continue with Tailscale",
|
||||
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
||||
"loginTailscaleDeviceName": "Device name:",
|
||||
"loginTailscaleOtherMethod": "Login with another method",
|
||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
|
||||
"quickActionsLanguage": "Language",
|
||||
"quickActionsTheme": "Theme",
|
||||
"quickActionsThemeLight": "Light",
|
||||
"quickActionsThemeDark": "Dark",
|
||||
"quickActionsThemeSystem": "System",
|
||||
"quickActionsLogout": "Logout",
|
||||
"quickActionsTitle": "Quick Actions",
|
||||
"quickActionsProviderLocal": "Local",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "已授权",
|
||||
"authorizeSuccessSubtitle": "您将在几秒钟内被重定向到应用程序。",
|
||||
"authorizeErrorClientInfo": "加载客户端信息时发生错误。请稍后再试。",
|
||||
"authorizeErrorMissingParams": "参数缺失:{{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "请求缺少所需参数或参数无效。请检查 URL 并重试。",
|
||||
"openidScopeName": "OpenID 连接",
|
||||
"openidScopeDescription": "允许应用访问您的 OpenID 连接信息。",
|
||||
"emailScopeName": "邮箱",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "个人资料",
|
||||
"profileScopeDescription": "允许应用访问您的个人信息。",
|
||||
"groupsScopeName": "分组",
|
||||
"groupsScopeDescription": "允许应用程序访问您的群组信息。"
|
||||
"groupsScopeDescription": "允许应用程序访问您的群组信息。",
|
||||
"backToLoginButton": "返回登录界面",
|
||||
"phoneScopeName": "手机",
|
||||
"phoneScopeDescription": "允许此应用程序访问您的手机号码。",
|
||||
"addressScopeName": "地址",
|
||||
"addressScopeDescription": "允许此应用程序访问您的地址。",
|
||||
"loginTailscaleTitle": "使用 Tailscale 继续",
|
||||
"loginTailscaleDescription": "您似乎正在使用已授权的 Tailscale 设备访问 Tinyauth 。是否要继续使用您的 Tailscale 连接?",
|
||||
"loginTailscaleDeviceName": "设备名称:",
|
||||
"loginTailscaleOtherMethod": "使用其他方式登录",
|
||||
"loginTailscaleSuccess": "已成功使用 Tailscale 进行身份验证。",
|
||||
"loginTailscaleFail": "使用 Tailscale 进行身份验证失败。请重试或使用其他登录方式。",
|
||||
"logoutTailscaleSubtitle": "您当前已通过 Tailscale 在名为<code>{{deviceName}}</code>的设备上登录。点击下方按钮注销。",
|
||||
"quickActionsLanguage": "语言",
|
||||
"quickActionsTheme": "主题",
|
||||
"quickActionsThemeLight": "亮色主题",
|
||||
"quickActionsThemeDark": "暗色主题",
|
||||
"quickActionsThemeSystem": "跟随系统",
|
||||
"quickActionsLogout": "登出",
|
||||
"quickActionsTitle": "快速操作",
|
||||
"quickActionsProviderLocal": "本地",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"authorizeSuccessTitle": "已授權",
|
||||
"authorizeSuccessSubtitle": "幾秒鐘內您將會被重新導向至應用程式。",
|
||||
"authorizeErrorClientInfo": "載入用戶端資訊時發生錯誤。請稍後再試。",
|
||||
"authorizeErrorMissingParams": "下列參數遺失:{{missingParams}}",
|
||||
"authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
|
||||
"openidScopeName": "OpenID 連接",
|
||||
"openidScopeDescription": "允許該應用程式存取您的 OpenID Connect 資訊。",
|
||||
"emailScopeName": "電子郵件",
|
||||
@@ -79,5 +79,27 @@
|
||||
"profileScopeName": "個人檔案",
|
||||
"profileScopeDescription": "允許該應用程式存取您的個人資料。",
|
||||
"groupsScopeName": "群組",
|
||||
"groupsScopeDescription": "允許該應用程式存取您的群組資訊。"
|
||||
"groupsScopeDescription": "允許該應用程式存取您的群組資訊。",
|
||||
"backToLoginButton": "Back to login",
|
||||
"phoneScopeName": "Phone",
|
||||
"phoneScopeDescription": "Allows the app to access your phone number.",
|
||||
"addressScopeName": "Address",
|
||||
"addressScopeDescription": "Allows the app to access your address.",
|
||||
"loginTailscaleTitle": "Continue with Tailscale",
|
||||
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
||||
"loginTailscaleDeviceName": "Device name:",
|
||||
"loginTailscaleOtherMethod": "Login with another method",
|
||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
|
||||
"quickActionsLanguage": "Language",
|
||||
"quickActionsTheme": "Theme",
|
||||
"quickActionsThemeLight": "Light",
|
||||
"quickActionsThemeDark": "Dark",
|
||||
"quickActionsThemeSystem": "System",
|
||||
"quickActionsLogout": "Logout",
|
||||
"quickActionsTitle": "Quick Actions",
|
||||
"quickActionsProviderLocal": "Local",
|
||||
"quickActionsProviderLDAP": "LDAP",
|
||||
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||
}
|
||||
|
||||
@@ -35,7 +35,10 @@ createRoot(document.getElementById("root")!).render(
|
||||
<Route element={<Layout />} errorElement={<ErrorPage />}>
|
||||
<Route path="/" element={<App />} />
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/authorize" element={<AuthorizePage />} />
|
||||
<Route
|
||||
path="/oidc/authorize"
|
||||
element={<AuthorizePage />}
|
||||
/>
|
||||
<Route path="/logout" element={<LogoutPage />} />
|
||||
<Route path="/continue" element={<ContinuePage />} />
|
||||
<Route path="/totp" element={<TotpPage />} />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useUserContext } from "@/context/user-context";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Navigate, useNavigate } from "react-router";
|
||||
import { useLocation } from "react-router";
|
||||
import {
|
||||
@@ -10,11 +10,9 @@ import {
|
||||
CardFooter,
|
||||
CardContent,
|
||||
} from "@/components/ui/card";
|
||||
import { getOidcClientInfoSchema } from "@/schemas/oidc-schemas";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import axios from "axios";
|
||||
import { toast } from "sonner";
|
||||
import { useOIDCParams } from "@/lib/hooks/oidc";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TFunction } from "i18next";
|
||||
import { Mail, MapPin, Phone, Shield, User, Users } from "lucide-react";
|
||||
@@ -23,6 +21,11 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import {
|
||||
recompileScreenParams,
|
||||
useScreenParams,
|
||||
} from "@/lib/hooks/screen-params";
|
||||
import { useEffect } from "react";
|
||||
|
||||
type Scope = {
|
||||
id: string;
|
||||
@@ -84,27 +87,25 @@ export const AuthorizePage = () => {
|
||||
const scopeMap = createScopeMap(t);
|
||||
|
||||
const searchParams = new URLSearchParams(search);
|
||||
const oidcParams = useOIDCParams(searchParams);
|
||||
const screenParams = useScreenParams(searchParams);
|
||||
const isOidc = screenParams.login_for === "oidc";
|
||||
const compiledParams = recompileScreenParams(screenParams);
|
||||
|
||||
const getClientInfo = useQuery({
|
||||
queryKey: ["client", oidcParams.values.client_id],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`/api/oidc/clients/${encodeURIComponent(oidcParams.values.client_id)}`,
|
||||
);
|
||||
const data = await getOidcClientInfoSchema.parseAsync(await res.json());
|
||||
return data;
|
||||
},
|
||||
enabled: oidcParams.isOidc,
|
||||
});
|
||||
// TODO: maybe a better way to do this
|
||||
const shouldAutoAuthorize =
|
||||
auth.authenticated &&
|
||||
isOidc &&
|
||||
screenParams.oidc_ticket !== undefined &&
|
||||
screenParams.oidc_scope !== undefined &&
|
||||
screenParams.oidc_prompt === "none";
|
||||
|
||||
const authorizeMutation = useMutation({
|
||||
const { mutate: authorizeMutate, isPending: authorizePending } = useMutation({
|
||||
mutationFn: () => {
|
||||
return axios.post("/api/oidc/authorize", {
|
||||
...oidcParams.values,
|
||||
return axios.post("/api/oidc/authorize-complete", {
|
||||
ticket: screenParams.oidc_ticket,
|
||||
});
|
||||
},
|
||||
mutationKey: ["authorize", oidcParams.values.client_id],
|
||||
mutationKey: ["authorize", screenParams.oidc_ticket],
|
||||
onSuccess: (data) => {
|
||||
toast.info(t("authorizeSuccessTitle"), {
|
||||
description: t("authorizeSuccessSubtitle"),
|
||||
@@ -118,56 +119,38 @@ export const AuthorizePage = () => {
|
||||
},
|
||||
});
|
||||
|
||||
if (oidcParams.issues.length > 0) {
|
||||
useEffect(() => {
|
||||
if (shouldAutoAuthorize) {
|
||||
authorizeMutate();
|
||||
}
|
||||
}, [shouldAutoAuthorize, authorizeMutate]);
|
||||
|
||||
if (!isOidc || !screenParams.oidc_ticket || !screenParams.oidc_scope) {
|
||||
return (
|
||||
<Navigate
|
||||
to={`/error?error=${encodeURIComponent(t("authorizeErrorMissingParams", { missingParams: oidcParams.issues.join(", ") }))}`}
|
||||
to={`/error?error=${encodeURIComponent(t("authorizeErrorInvalidParams"))}`}
|
||||
replace
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!auth.authenticated) {
|
||||
return <Navigate to={`/login?${oidcParams.compiled}`} replace />;
|
||||
}
|
||||
|
||||
if (getClientInfo.isLoading) {
|
||||
return (
|
||||
<Card className="gap-0">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">
|
||||
{t("authorizeLoadingTitle")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription>{t("authorizeLoadingSubtitle")}</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (getClientInfo.isError) {
|
||||
return (
|
||||
<Navigate
|
||||
to={`/error?error=${encodeURIComponent(t("authorizeErrorClientInfo"))}`}
|
||||
replace
|
||||
/>
|
||||
);
|
||||
if (!auth.authenticated || screenParams.oidc_prompt === "login") {
|
||||
return <Navigate to={`/login${compiledParams}`} replace />;
|
||||
}
|
||||
|
||||
const scopes =
|
||||
oidcParams.values.scope.split(" ").filter((s) => s.trim() !== "") || [];
|
||||
screenParams.oidc_scope.split(" ").filter((s) => s.trim() !== "") || [];
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="mb-2">
|
||||
<div className="flex flex-col gap-3 items-center justify-center text-center">
|
||||
<div className="bg-accent-foreground box-content text-muted text-xl font-bold font-sans rounded-lg size-8 p-2 flex items-center justify-center">
|
||||
{getClientInfo.data?.name.slice(0, 1) || "U"}
|
||||
{screenParams.oidc_name ? screenParams.oidc_name.slice(0, 1) : "U"}
|
||||
</div>
|
||||
<CardTitle className="text-xl">
|
||||
{t("authorizeCardTitle", {
|
||||
app: getClientInfo.data?.name || "Unknown",
|
||||
app: screenParams.oidc_name || "Unknown",
|
||||
})}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-sm max-w-sm">
|
||||
@@ -200,14 +183,14 @@ export const AuthorizePage = () => {
|
||||
)}
|
||||
<CardFooter className="flex flex-col items-stretch gap-3">
|
||||
<Button
|
||||
onClick={() => authorizeMutation.mutate()}
|
||||
loading={authorizeMutation.isPending}
|
||||
onClick={() => authorizeMutate()}
|
||||
loading={authorizePending || shouldAutoAuthorize}
|
||||
>
|
||||
{t("authorizeTitle")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => navigate("/")}
|
||||
disabled={authorizeMutation.isPending}
|
||||
onClick={() => navigate(`/logout${compiledParams}`)}
|
||||
disabled={authorizePending || shouldAutoAuthorize}
|
||||
variant="outline"
|
||||
>
|
||||
{t("cancelTitle")}
|
||||
|
||||
@@ -12,6 +12,10 @@ import { Trans, useTranslation } from "react-i18next";
|
||||
import { Navigate, useLocation, useNavigate } from "react-router";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useRedirectUri } from "@/lib/hooks/redirect-uri";
|
||||
import {
|
||||
recompileScreenParams,
|
||||
useScreenParams,
|
||||
} from "@/lib/hooks/screen-params";
|
||||
|
||||
export const ContinuePage = () => {
|
||||
const { app, ui } = useAppContext();
|
||||
@@ -25,11 +29,16 @@ export const ContinuePage = () => {
|
||||
const hasRedirected = useRef(false);
|
||||
|
||||
const searchParams = new URLSearchParams(search);
|
||||
const redirectUri = searchParams.get("redirect_uri");
|
||||
const screenParams = useScreenParams(searchParams);
|
||||
const redirectUri = screenParams.redirect_uri;
|
||||
const isAppLogin = screenParams.login_for === "app";
|
||||
const recompiledParams = recompileScreenParams(screenParams);
|
||||
|
||||
const { url, valid, trusted, allowedProto, httpsDowngrade } = useRedirectUri(
|
||||
redirectUri,
|
||||
app.cookieDomain,
|
||||
app.appUrl,
|
||||
app.subdomainsEnabled,
|
||||
);
|
||||
|
||||
const urlHref = url?.href;
|
||||
@@ -43,7 +52,8 @@ export const ContinuePage = () => {
|
||||
auth.authenticated &&
|
||||
hasValidRedirect &&
|
||||
!showUntrustedWarning &&
|
||||
!showInsecureWarning;
|
||||
!showInsecureWarning &&
|
||||
isAppLogin;
|
||||
|
||||
const redirectToTarget = useCallback(() => {
|
||||
if (!urlHref || hasRedirected.current) {
|
||||
@@ -79,15 +89,10 @@ export const ContinuePage = () => {
|
||||
}, [shouldAutoRedirect, redirectToTarget]);
|
||||
|
||||
if (!auth.authenticated) {
|
||||
return (
|
||||
<Navigate
|
||||
to={`/login${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`}
|
||||
replace
|
||||
/>
|
||||
);
|
||||
return <Navigate to={`/login${recompiledParams}`} replace />;
|
||||
}
|
||||
|
||||
if (!hasValidRedirect) {
|
||||
if (!hasValidRedirect || !isAppLogin) {
|
||||
return <Navigate to="/logout" replace />;
|
||||
}
|
||||
|
||||
@@ -105,7 +110,11 @@ export const ContinuePage = () => {
|
||||
components={{
|
||||
code: <code />,
|
||||
}}
|
||||
values={{ cookieDomain: app.cookieDomain }}
|
||||
values={{
|
||||
cookieDomain: app.subdomainsEnabled
|
||||
? `.${app.cookieDomain}`
|
||||
: app.cookieDomain,
|
||||
}}
|
||||
shouldUnescape={true}
|
||||
/>
|
||||
</CardDescription>
|
||||
|
||||
@@ -11,7 +11,7 @@ export const ErrorPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { search } = useLocation();
|
||||
const searchParams = new URLSearchParams(search);
|
||||
const error = searchParams.get("error") ?? "";
|
||||
const error = searchParams.get("error") || "";
|
||||
|
||||
return (
|
||||
<Card>
|
||||
|
||||
@@ -11,12 +11,18 @@ import { useAppContext } from "@/context/app-context";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Markdown from "react-markdown";
|
||||
import { useLocation } from "react-router";
|
||||
import {
|
||||
recompileScreenParams,
|
||||
useScreenParams,
|
||||
} from "@/lib/hooks/screen-params";
|
||||
|
||||
export const ForgotPasswordPage = () => {
|
||||
const { ui } = useAppContext();
|
||||
const { t } = useTranslation();
|
||||
const { search } = useLocation();
|
||||
const searchParams = new URLSearchParams(search);
|
||||
const screenParams = useScreenParams(searchParams);
|
||||
const compiledParams = recompileScreenParams(screenParams);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@@ -37,10 +43,7 @@ export const ForgotPasswordPage = () => {
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const eparams = searchParams.toString();
|
||||
window.location.replace(
|
||||
`/login${eparams.length > 0 ? `?${eparams}` : ""}`,
|
||||
);
|
||||
window.location.replace(`/login${compiledParams}`);
|
||||
}}
|
||||
>
|
||||
{t("backToLoginButton")}
|
||||
|
||||
@@ -18,7 +18,6 @@ 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 { useOIDCParams } from "@/lib/hooks/oidc";
|
||||
import { LoginSchema } from "@/schemas/login-schema";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import axios, { AxiosError } from "axios";
|
||||
@@ -26,6 +25,11 @@ import { useEffect, useId, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Navigate, useLocation } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
recompileScreenParams,
|
||||
useScreenParams,
|
||||
} from "@/lib/hooks/screen-params";
|
||||
import { useLoginFor } from "@/lib/hooks/login-for";
|
||||
|
||||
const iconMap: Record<string, React.ReactNode> = {
|
||||
google: <GoogleIcon />,
|
||||
@@ -46,7 +50,9 @@ export const LoginPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [showRedirectButton, setShowRedirectButton] = useState(false);
|
||||
const [useTailscale, setUseTailscale] = useState(tailscale.nodeName !== undefined);
|
||||
const [useTailscale, setUseTailscale] = useState(
|
||||
tailscale.nodeName !== undefined,
|
||||
);
|
||||
|
||||
const hasAutoRedirectedRef = useRef(false);
|
||||
|
||||
@@ -56,17 +62,25 @@ export const LoginPage = () => {
|
||||
const formId = useId();
|
||||
|
||||
const searchParams = new URLSearchParams(search);
|
||||
const redirectUri = searchParams.get("redirect_uri") || undefined;
|
||||
const oidcParams = useOIDCParams(searchParams);
|
||||
const screenParams = useScreenParams(searchParams);
|
||||
const compiledParams = recompileScreenParams({
|
||||
...screenParams,
|
||||
oidc_prompt: undefined,
|
||||
});
|
||||
const loginForUrl = useLoginFor({
|
||||
login_for: screenParams.login_for,
|
||||
compiledParams,
|
||||
});
|
||||
|
||||
const [isOauthAutoRedirect, setIsOauthAutoRedirect] = useState(
|
||||
providers.find((provider) => provider.id === oauth.autoRedirect) !==
|
||||
undefined && redirectUri !== undefined,
|
||||
undefined && screenParams.redirect_uri !== undefined,
|
||||
);
|
||||
|
||||
const oauthProviders = providers.filter(
|
||||
(provider) => provider.id !== "local" && provider.id !== "ldap",
|
||||
);
|
||||
|
||||
const userAuthConfigured =
|
||||
providers.find(
|
||||
(provider) => provider.id === "local" || provider.id === "ldap",
|
||||
@@ -79,16 +93,7 @@ export const LoginPage = () => {
|
||||
variables: oauthVariables,
|
||||
} = useMutation({
|
||||
mutationFn: (provider: string) => {
|
||||
const getParams = function (): string {
|
||||
if (oidcParams.isOidc) {
|
||||
return `?${oidcParams.compiled}`;
|
||||
}
|
||||
if (redirectUri) {
|
||||
return `?redirect_uri=${encodeURIComponent(redirectUri)}`;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
return axios.get(`/api/oauth/url/${provider}${getParams()}`);
|
||||
return axios.get(`/api/oauth/url/${provider}${compiledParams}`);
|
||||
},
|
||||
mutationKey: ["oauth"],
|
||||
onSuccess: (data) => {
|
||||
@@ -119,13 +124,7 @@ export const LoginPage = () => {
|
||||
mutationKey: ["login"],
|
||||
onSuccess: (data) => {
|
||||
if (data.data.totpPending) {
|
||||
if (oidcParams.isOidc) {
|
||||
window.location.replace(`/totp?${oidcParams.compiled}`);
|
||||
return;
|
||||
}
|
||||
window.location.replace(
|
||||
`/totp${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
|
||||
);
|
||||
window.location.replace(`/totp${compiledParams}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -134,13 +133,7 @@ export const LoginPage = () => {
|
||||
});
|
||||
|
||||
redirectTimer.current = window.setTimeout(() => {
|
||||
if (oidcParams.isOidc) {
|
||||
window.location.replace(`/authorize?${oidcParams.compiled}`);
|
||||
return;
|
||||
}
|
||||
window.location.replace(
|
||||
`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
|
||||
);
|
||||
window.location.replace(loginForUrl);
|
||||
}, 500);
|
||||
},
|
||||
onError: (error: AxiosError) => {
|
||||
@@ -163,13 +156,7 @@ export const LoginPage = () => {
|
||||
});
|
||||
|
||||
redirectTimer.current = window.setTimeout(() => {
|
||||
if (oidcParams.isOidc) {
|
||||
window.location.replace(`/authorize?${oidcParams.compiled}`);
|
||||
return;
|
||||
}
|
||||
window.location.replace(
|
||||
`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
|
||||
);
|
||||
window.location.replace(loginForUrl);
|
||||
}, 500);
|
||||
},
|
||||
onError: () => {
|
||||
@@ -184,7 +171,8 @@ export const LoginPage = () => {
|
||||
!auth.authenticated &&
|
||||
isOauthAutoRedirect &&
|
||||
!hasAutoRedirectedRef.current &&
|
||||
redirectUri !== undefined
|
||||
screenParams.redirect_uri &&
|
||||
screenParams.login_for
|
||||
) {
|
||||
hasAutoRedirectedRef.current = true;
|
||||
oauthMutate(oauth.autoRedirect);
|
||||
@@ -195,7 +183,8 @@ export const LoginPage = () => {
|
||||
hasAutoRedirectedRef,
|
||||
oauth.autoRedirect,
|
||||
isOauthAutoRedirect,
|
||||
redirectUri,
|
||||
screenParams.login_for,
|
||||
screenParams.redirect_uri,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -210,21 +199,8 @@ export const LoginPage = () => {
|
||||
};
|
||||
}, [redirectTimer, redirectButtonTimer]);
|
||||
|
||||
if (auth.authenticated && oidcParams.isOidc) {
|
||||
return <Navigate to={`/authorize?${oidcParams.compiled}`} replace />;
|
||||
}
|
||||
|
||||
if (auth.authenticated && redirectUri !== undefined) {
|
||||
return (
|
||||
<Navigate
|
||||
to={`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`}
|
||||
replace
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (auth.authenticated) {
|
||||
return <Navigate to="/logout" replace />;
|
||||
if (auth.authenticated && screenParams.oidc_prompt !== "login") {
|
||||
return <Navigate to={loginForUrl} replace />;
|
||||
}
|
||||
|
||||
if (isOauthAutoRedirect) {
|
||||
@@ -283,7 +259,7 @@ export const LoginPage = () => {
|
||||
onClick={() => tailscaleMutate()}
|
||||
loading={tailscaleIsPending}
|
||||
>
|
||||
{t("loginTailscaleSubmit")}
|
||||
{t("loginTailscaleTitle")}
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full"
|
||||
|
||||
@@ -15,12 +15,21 @@ import { Navigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { type UseMutationResult } from "@tanstack/react-query";
|
||||
import { type AxiosResponse } from "axios";
|
||||
import { useLocation } from "react-router";
|
||||
import {
|
||||
useScreenParams,
|
||||
recompileScreenParams,
|
||||
} from "@/lib/hooks/screen-params";
|
||||
|
||||
export const LogoutPage = () => {
|
||||
const { auth, oauth, tailscale } = useUserContext();
|
||||
const { t } = useTranslation();
|
||||
const { search } = useLocation();
|
||||
|
||||
const redirectTimer = useRef<number | null>(null);
|
||||
const searchParams = new URLSearchParams(search);
|
||||
const screenParams = useScreenParams(searchParams);
|
||||
const compiledParams = recompileScreenParams(screenParams);
|
||||
|
||||
const logoutMutation = useMutation({
|
||||
mutationFn: () => axios.post("/api/user/logout"),
|
||||
@@ -31,7 +40,7 @@ export const LogoutPage = () => {
|
||||
});
|
||||
|
||||
redirectTimer.current = window.setTimeout(() => {
|
||||
window.location.replace("/login");
|
||||
window.location.replace(`/login${compiledParams}`);
|
||||
}, 500);
|
||||
},
|
||||
onError: () => {
|
||||
@@ -50,7 +59,7 @@ export const LogoutPage = () => {
|
||||
}, [redirectTimer]);
|
||||
|
||||
if (!auth.authenticated) {
|
||||
return <Navigate to="/login" replace />;
|
||||
return <Navigate to={`/login${compiledParams}`} replace />;
|
||||
}
|
||||
|
||||
if (oauth.active) {
|
||||
@@ -128,7 +137,7 @@ function LogoutLayout({ children, logoutMutation }: LogoutLayoutProps) {
|
||||
</CardHeader>
|
||||
<CardFooter>
|
||||
<Button
|
||||
className="w-full"
|
||||
className="w-full text-destructive"
|
||||
variant="outline"
|
||||
loading={logoutMutation.isPending}
|
||||
onClick={() => logoutMutation.mutate()}
|
||||
|
||||
@@ -16,10 +16,14 @@ import { useEffect, useId, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Navigate, useLocation } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { useOIDCParams } from "@/lib/hooks/oidc";
|
||||
import {
|
||||
recompileScreenParams,
|
||||
useScreenParams,
|
||||
} from "@/lib/hooks/screen-params";
|
||||
import { useLoginFor } from "@/lib/hooks/login-for";
|
||||
|
||||
export const TotpPage = () => {
|
||||
const { totp } = useUserContext();
|
||||
const { totp, auth } = useUserContext();
|
||||
const { t } = useTranslation();
|
||||
const { search } = useLocation();
|
||||
const formId = useId();
|
||||
@@ -27,8 +31,12 @@ export const TotpPage = () => {
|
||||
const redirectTimer = useRef<number | null>(null);
|
||||
|
||||
const searchParams = new URLSearchParams(search);
|
||||
const redirectUri = searchParams.get("redirect_uri") || undefined;
|
||||
const oidcParams = useOIDCParams(searchParams);
|
||||
const screenParams = useScreenParams(searchParams);
|
||||
const compiledParams = recompileScreenParams(screenParams);
|
||||
const loginForUrl = useLoginFor({
|
||||
login_for: screenParams.login_for,
|
||||
compiledParams,
|
||||
});
|
||||
|
||||
const totpMutation = useMutation({
|
||||
mutationFn: (values: TotpSchema) => axios.post("/api/user/totp", values),
|
||||
@@ -39,14 +47,7 @@ export const TotpPage = () => {
|
||||
});
|
||||
|
||||
redirectTimer.current = window.setTimeout(() => {
|
||||
if (oidcParams.isOidc) {
|
||||
window.location.replace(`/authorize?${oidcParams.compiled}`);
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.replace(
|
||||
`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
|
||||
);
|
||||
window.location.replace(loginForUrl);
|
||||
}, 500);
|
||||
},
|
||||
onError: () => {
|
||||
@@ -65,7 +66,10 @@ export const TotpPage = () => {
|
||||
}, [redirectTimer]);
|
||||
|
||||
if (!totp.pending) {
|
||||
return <Navigate to="/" replace />;
|
||||
if (auth.authenticated) {
|
||||
return <Navigate to={loginForUrl} replace />;
|
||||
}
|
||||
return <Navigate to={`/login${compiledParams}`} replace />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -24,7 +24,7 @@ const uiSchema = z.object({
|
||||
const appSchema = z.object({
|
||||
appUrl: z.string(),
|
||||
cookieDomain: z.string(),
|
||||
trustedDomains: z.array(z.string()),
|
||||
subdomainsEnabled: z.boolean(),
|
||||
});
|
||||
|
||||
export const appContextSchema = z.object({
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const getOidcClientInfoSchema = z.object({
|
||||
name: z.string(),
|
||||
});
|
||||
@@ -57,6 +57,11 @@ export default defineConfig({
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/robots.txt/, ""),
|
||||
},
|
||||
"/authorize": {
|
||||
target: "http://tinyauth-backend:3000/authorize",
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/authorize/, ""),
|
||||
},
|
||||
},
|
||||
allowedHosts: true,
|
||||
},
|
||||
|
||||
@@ -20,7 +20,7 @@ type EnvEntry struct {
|
||||
}
|
||||
|
||||
func generateExampleEnv() {
|
||||
cfg := model.NewDefaultConfiguration()
|
||||
cfg := model.NewDefaultConfiguration(model.RuntimeEnvUnknown)
|
||||
entries := make([]EnvEntry, 0)
|
||||
|
||||
root := reflect.TypeOf(cfg).Elem()
|
||||
@@ -21,7 +21,7 @@ type MarkdownEntry struct {
|
||||
}
|
||||
|
||||
func generateMarkdown() {
|
||||
cfg := model.NewDefaultConfiguration()
|
||||
cfg := model.NewDefaultConfiguration(model.RuntimeEnvUnknown)
|
||||
entries := make([]MarkdownEntry, 0)
|
||||
|
||||
root := reflect.TypeOf(cfg).Elem()
|
||||
@@ -1,4 +1,4 @@
|
||||
// gen/sqlc-wrapper generates store.go wrapper files for each sqlc driver package under
|
||||
// gen/sqlc_wrapper generates store.go wrapper files for each sqlc driver package under
|
||||
// internal/repository/<driver>/. Run via:
|
||||
//
|
||||
// go generate ./internal/repository/...
|
||||
@@ -32,7 +32,7 @@ import (
|
||||
var storeSrc string
|
||||
|
||||
func main() {
|
||||
fmt.Println("sqlc-wrapper: generating store.go files for sqlc driver packages...")
|
||||
fmt.Println("sqlc_wrapper: generating store.go files for sqlc driver packages...")
|
||||
if err := run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package tinyauth
|
||||
|
||||
//go:generate go run github.com/tinyauthapp/tinyauth/gen/docs
|
||||
@@ -1,69 +1,57 @@
|
||||
module github.com/tinyauthapp/tinyauth
|
||||
|
||||
go 1.26.1
|
||||
go 1.26.4
|
||||
|
||||
require (
|
||||
charm.land/huh/v2 v2.0.3
|
||||
charm.land/lipgloss/v2 v2.0.5
|
||||
github.com/cenkalti/backoff/v5 v5.0.3
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/go-jose/go-jose/v4 v4.1.4
|
||||
github.com/go-ldap/ldap/v3 v3.4.13
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/golang-migrate/migrate/v4 v4.19.1
|
||||
github.com/google/go-querystring v1.2.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jackc/pgx/v5 v5.10.0
|
||||
github.com/mdp/qrterminal/v3 v3.2.1
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/rs/zerolog v1.35.1
|
||||
github.com/steveiliop56/ding v0.2.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298
|
||||
github.com/weppos/publicsuffix-go v0.50.3
|
||||
golang.org/x/crypto v0.50.0
|
||||
go.uber.org/dig v1.19.0
|
||||
golang.org/x/crypto v0.53.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/tools v0.43.0
|
||||
k8s.io/apimachinery v0.36.0
|
||||
k8s.io/client-go v0.36.0
|
||||
modernc.org/sqlite v1.50.0
|
||||
tailscale.com v1.96.5
|
||||
golang.org/x/tools v0.47.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/apimachinery v0.36.2
|
||||
k8s.io/client-go v0.36.2
|
||||
modernc.org/sqlite v1.53.0
|
||||
)
|
||||
|
||||
require (
|
||||
charm.land/bubbles/v2 v2.0.0 // indirect
|
||||
charm.land/bubbletea/v2 v2.0.2 // indirect
|
||||
charm.land/lipgloss/v2 v2.0.1 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.1.1 // indirect
|
||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/akutz/memconn v0.1.0 // indirect
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.58 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
|
||||
github.com/aws/smithy-go v1.24.0 // indirect
|
||||
github.com/boombuler/barcode v1.0.2 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/catppuccin/go v0.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.2 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.7 // indirect
|
||||
github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
@@ -72,60 +60,46 @@ require (
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/coder/websocket v1.8.12 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/creachadair/msync v0.7.1 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/gaissmai/bart v0.26.1 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.4 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/huin/goupnp v1.3.0 // indirect
|
||||
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.9.2 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.23 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
|
||||
github.com/mdlayher/socket v0.5.0 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
@@ -134,23 +108,16 @@ require (
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus-community/pro-bing v0.4.0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/safchain/ethtool v0.3.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
|
||||
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect
|
||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
|
||||
github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
@@ -165,28 +132,22 @@ require (
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/arch v0.22.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/mod v0.34.0 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/term v0.42.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
golang.org/x/mod v0.37.0 // indirect
|
||||
golang.org/x/net v0.56.0 // indirect
|
||||
golang.org/x/sync v0.21.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
golang.org/x/term v0.44.0 // indirect
|
||||
golang.org/x/text v0.38.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gotest.tools/v3 v3.5.2 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 // indirect
|
||||
k8s.io/klog/v2 v2.140.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
|
||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
|
||||
modernc.org/libc v1.72.0 // indirect
|
||||
modernc.org/libc v1.73.4 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
rsc.io/qr v0.2.0 // indirect
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f h1:1C7nZuxUMNz7eiQALRfiqNOm04+m3edWlRff/BYHf0Q=
|
||||
9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f/go.mod h1:hHyrZRryGqVdqrknjq5OWDLGCTJ2NeEvtrpR96mjraM=
|
||||
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
|
||||
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
|
||||
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
|
||||
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
|
||||
charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU=
|
||||
charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc=
|
||||
charm.land/lipgloss/v2 v2.0.1 h1:6Xzrn49+Py1Um5q/wZG1gWgER2+7dUyZ9XMEufqPSys=
|
||||
charm.land/lipgloss/v2 v2.0.1/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
|
||||
charm.land/lipgloss/v2 v2.0.5 h1:kbNxgeeUOYv5J0YdpxFjfvf3dFvqH8Aci4zB6xqFtrY=
|
||||
charm.land/lipgloss/v2 v2.0.5/go.mod h1:9oqhxt4yxIMe6q5A4kHr44DremZk7J9UNh74GlWa5nc=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc=
|
||||
filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
|
||||
@@ -30,44 +24,10 @@ github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe
|
||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
|
||||
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
|
||||
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/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.5 h1:4lS2IB+wwkj5J43Tq/AwvnscBerBJtQQ6YS7puzCI1k=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.5/go.mod h1:SNzldMlDVbN6nWxM7XsUiNXPSa1LWlqiXtvh/1PrJGg=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.58 h1:/d7FUpAPU8Lf2KUdjniQvfNdlMID0Sd9pS23FJ3SS9Y=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.58/go.mod h1:aVYW33Ow10CyMQGFgC0ptMRIqJWvJ4nxZb0sUiuQT/A=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 h1:7lOW8NUwE9UZekS1DYoiPdVAqZ6A+LheHWb+mHbNOq8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27/go.mod h1:w1BASFIPOPUae7AgaH4SbjNbfdkxuggLyGfNFTn8ITY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 h1:a8HvP/+ew3tKwSXqL3BCSjiuicr+XTU2eFYeogV9GJE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14/go.mod h1:+JJQTxB6N4niArC14YNtxcQtwEqzS3o9Z32n7q33Rfs=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 h1:f1L/JtUkVODD+k1+IiSJUUv8A++2qVr+Xvb3xWXETMU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13/go.mod h1:tvqlFoja8/s0o+UruA1Nrezo/df0PzdunMDDurUfg6U=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
|
||||
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
|
||||
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02 h1:bXAPYSbdYbS5VTy92NIUbeDI1qyggi+JYh5op9IFlcQ=
|
||||
github.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c=
|
||||
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
|
||||
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
@@ -85,12 +45,12 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
|
||||
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
|
||||
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
||||
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
|
||||
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
|
||||
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
|
||||
github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
|
||||
github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk=
|
||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
|
||||
@@ -109,50 +69,32 @@ github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2
|
||||
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
||||
github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw=
|
||||
github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y=
|
||||
github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok=
|
||||
github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE=
|
||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
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=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=
|
||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
||||
github.com/creachadair/mds v0.25.9 h1:080Hr8laN2h+l3NeVCGMBpXtIPnl9mz8e4HLraGPqtA=
|
||||
github.com/creachadair/mds v0.25.9/go.mod h1:4hatI3hRM+qhzuAmqPRFvaBM8mONkS7nsLxkcuTYUIs=
|
||||
github.com/creachadair/msync v0.7.1 h1:SeZmuEBXQPe5GqV/C94ER7QIZPwtvFbeQiykzt/7uho=
|
||||
github.com/creachadair/msync v0.7.1/go.mod h1:8CcFlLsSujfHE5wWm19uUBLHIPDAUr6LXDwneVMO008=
|
||||
github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc=
|
||||
github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g=
|
||||
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/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/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=
|
||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ=
|
||||
github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc h1:8WFBn63wegobsYAX0YjD+8suexZDga5CctH4CCTx2+8=
|
||||
github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
|
||||
github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
|
||||
github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU=
|
||||
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/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
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/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
@@ -167,20 +109,14 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gaissmai/bart v0.26.1 h1:+w4rnLGNlA2GDVn382Tfe3jOsK5vOr5n4KmigJ9lbTo=
|
||||
github.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
|
||||
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.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
|
||||
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
|
||||
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-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced h1:Q311OHjMh/u5E2TITc++WlTP5We0xNseRMkHDyvhW7I=
|
||||
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
|
||||
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
|
||||
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -188,8 +124,6 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
|
||||
@@ -204,20 +138,14 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737 h1:cf60tHxREO3g1nroKr2osU3JWZsJzkfi7rEg+oAB0Lo=
|
||||
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737/go.mod h1:MIS0jDzbU/vuM9MC4YnBITCv+RYuTRq8dJzmCrFsK9g=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
|
||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
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/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
@@ -225,11 +153,7 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
||||
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
||||
github.com/google/go-tpm v0.9.4 h1:awZRf9FwOeTunQmHoDYSHJps3ie6f1UlhS1fOdPEt1I=
|
||||
github.com/google/go-tpm v0.9.4/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI=
|
||||
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
@@ -238,27 +162,18 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
|
||||
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
|
||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
|
||||
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
||||
github.com/illarion/gonotify/v3 v3.0.2 h1:O7S6vcopHexutmpObkeWsnzMJt/r1hONIEogeVNmJMk=
|
||||
github.com/illarion/gonotify/v3 v3.0.2/go.mod h1:HWGPdPe817GfvY3w7cx6zkbzNZfi3QjcBm/wgVvEL1U=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
|
||||
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw=
|
||||
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
||||
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
|
||||
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
@@ -273,24 +188,12 @@ 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/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g=
|
||||
github.com/jellydator/ttlcache/v3 v3.1.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=
|
||||
github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E=
|
||||
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/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
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/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
|
||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
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=
|
||||
@@ -299,34 +202,22 @@ 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.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
|
||||
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
||||
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
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/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
||||
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=
|
||||
github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c=
|
||||
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
|
||||
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
|
||||
github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI=
|
||||
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/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
|
||||
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
@@ -353,37 +244,23 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
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.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
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/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
|
||||
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
|
||||
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
|
||||
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/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4=
|
||||
github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
|
||||
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
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.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
@@ -392,16 +269,16 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
||||
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||
github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
|
||||
github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
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/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
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/steveiliop56/ding v0.2.0 h1:m/Fj99wBpVVLHlpqb2RDJkWubOc5cWJ11ZYCHya3Sk0=
|
||||
github.com/steveiliop56/ding v0.2.0/go.mod h1:bE2u2XH7CjhPzbb/0Ems+D8YZlf2Ae+eKhj00UR1iAY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -415,40 +292,12 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ=
|
||||
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869 h1:SRL6irQkKGQKKLzvQP/ke/2ZuB7Py5+XuqtOgSj+iMM=
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=
|
||||
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
|
||||
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
|
||||
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU=
|
||||
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
|
||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA=
|
||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc=
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14=
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
||||
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M=
|
||||
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da h1:jVRUZPRs9sqyKlYHHzHjAqKN+6e/Vog6NpHYeNPJqOw=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
||||
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek=
|
||||
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
|
||||
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
|
||||
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
|
||||
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298 h1:EYSb5jv8ZL/0/NVFZtY7Ejplk0QG5+3lrdL3mSrjFZQ=
|
||||
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298/go.mod h1:TlUDoCF66hMqFZqoBym9bUdJ0bKAWYMir6hLJeYN5z0=
|
||||
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/u-root/u-root v0.14.0 h1:Ka4T10EEML7dQ5XDvO9c3MBN8z4nuSnGjcd1jmU2ivg=
|
||||
github.com/u-root/u-root v0.14.0/go.mod h1:hAyZorapJe4qzbLWlAkmSVCJGbfoU9Pu4jpJ1WMluqE=
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/weppos/publicsuffix-go v0.50.3 h1:eT5dcjHQcVDNc0igpFEsGHKIip30feuB2zuuI9eJxiE=
|
||||
github.com/weppos/publicsuffix-go v0.50.3/go.mod h1:/rOa781xBykZhHK/I3QeHo92qdDKVmKZKF7s8qAEM/4=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
@@ -477,51 +326,39 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4=
|
||||
go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8=
|
||||
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
|
||||
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
|
||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||
@@ -543,48 +380,42 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 h1:Zy8IV/+FMLxy6j6p87vk/vQGKcdnbprwjTxc8UiUtsA=
|
||||
gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=
|
||||
honnef.co/go/tools v0.7.0-0.dev.0.20251022135355-8273271481d0 h1:5SXjd4ET5dYijLaf0O3aOenC0Z4ZafIWSpjUzsQaNho=
|
||||
honnef.co/go/tools v0.7.0-0.dev.0.20251022135355-8273271481d0/go.mod h1:EPDDhEZqVHhWuPI5zPAsjU0U7v9xNIWjoOVyZ5ZcniQ=
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
k8s.io/api v0.36.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80=
|
||||
k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34=
|
||||
k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ=
|
||||
k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc=
|
||||
k8s.io/client-go v0.36.0 h1:pOYi7C4RHChYjMiHpZSpSbIM6ZxVbRXBy7CuiIwqA3c=
|
||||
k8s.io/client-go v0.36.0/go.mod h1:ZKKcpwF0aLYfkHFCjillCKaTK/yBkEDHTDXCFY6AS9Y=
|
||||
k8s.io/api v0.36.2 h1:TF6YDLIzKfccK7cq9YpTcGX8TJmEkHVRv78DM51fRYY=
|
||||
k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg=
|
||||
k8s.io/apimachinery v0.36.2 h1:0PE/W/WNy1UX61NLbXY5TMbJ6UwLL6E6lAPkYrKFxbQ=
|
||||
k8s.io/apimachinery v0.36.2/go.mod h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4=
|
||||
k8s.io/client-go v0.36.2 h1:bfgxmFKc9CgqsgX4xKLAAdmTQlWee7Ob/HlDOrJ5TBI=
|
||||
k8s.io/client-go v0.36.2/go.mod h1:1vgO4OAlfPnoLcb+Rze2GF5rAr14w8qjrYMoyXJzQj0=
|
||||
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
|
||||
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
|
||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg=
|
||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
|
||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
|
||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
||||
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
|
||||
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
|
||||
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
|
||||
modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
|
||||
modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c=
|
||||
modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws=
|
||||
modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc=
|
||||
modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
|
||||
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
|
||||
modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA=
|
||||
modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
|
||||
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/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||
modernc.org/opt v0.2.0/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.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM=
|
||||
modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
|
||||
modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M=
|
||||
modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s=
|
||||
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=
|
||||
@@ -599,7 +430,3 @@ sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
|
||||
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
|
||||
tailscale.com v1.96.5 h1:gNkfA/KSZAl6jCH9cj8urq00HRWItDDTtGsyATI89jA=
|
||||
tailscale.com v1.96.5/go.mod h1:/3lnZBYb2UEwnN0MNu2SDXUtT06AGd5k0s+OWx3WmcY=
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
DROP TABLE IF EXISTS "oidc_sessions";
|
||||
|
||||
CREATE TABLE "oidc_codes" (
|
||||
"sub" TEXT NOT NULL UNIQUE,
|
||||
"code_hash" TEXT NOT NULL PRIMARY KEY,
|
||||
"scope" TEXT NOT NULL,
|
||||
"redirect_uri" TEXT NOT NULL,
|
||||
"client_id" TEXT NOT NULL,
|
||||
"expires_at" BIGINT NOT NULL,
|
||||
"nonce" TEXT NOT NULL DEFAULT '',
|
||||
"code_challenge" TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE TABLE "oidc_tokens" (
|
||||
"sub" TEXT NOT NULL UNIQUE,
|
||||
"access_token_hash" TEXT NOT NULL PRIMARY KEY,
|
||||
"refresh_token_hash" TEXT NOT NULL,
|
||||
"code_hash" TEXT NOT NULL,
|
||||
"scope" TEXT NOT NULL,
|
||||
"client_id" TEXT NOT NULL,
|
||||
"token_expires_at" BIGINT NOT NULL,
|
||||
"refresh_token_expires_at" BIGINT NOT NULL,
|
||||
"nonce" TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE TABLE "oidc_userinfo" (
|
||||
"sub" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"preferred_username" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"groups" TEXT NOT NULL,
|
||||
"updated_at" BIGINT NOT NULL,
|
||||
"given_name" TEXT NOT NULL,
|
||||
"family_name" TEXT NOT NULL,
|
||||
"middle_name" TEXT NOT NULL,
|
||||
"nickname" TEXT NOT NULL,
|
||||
"profile" TEXT NOT NULL,
|
||||
"picture" TEXT NOT NULL,
|
||||
"website" TEXT NOT NULL,
|
||||
"gender" TEXT NOT NULL,
|
||||
"birthdate" TEXT NOT NULL,
|
||||
"zoneinfo" TEXT NOT NULL,
|
||||
"locale" TEXT NOT NULL,
|
||||
"phone_number" TEXT NOT NULL,
|
||||
"address" TEXT NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
This migration will nuke the entire setup of OIDC sessions and merge everything
|
||||
into one table.
|
||||
*/
|
||||
|
||||
/*
|
||||
Drop all the old tables. Yes, we will log out all OIDC users, but not really a big deal
|
||||
*/
|
||||
|
||||
DROP TABLE IF EXISTS "oidc_tokens";
|
||||
DROP TABLE IF EXISTS "oidc_userinfo";
|
||||
DROP TABLE IF EXISTS "oidc_codes";
|
||||
|
||||
/*
|
||||
Create a new simple OIDC sessions table that will hold tokens + userinfo.
|
||||
*/
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "oidc_sessions" (
|
||||
"sub" TEXT NOT NULL UNIQUE PRIMARY KEY,
|
||||
"access_token_hash" TEXT NOT NULL UNIQUE,
|
||||
"refresh_token_hash" TEXT NOT NULL UNIQUE,
|
||||
"scope" TEXT NOT NULL,
|
||||
"client_id" TEXT NOT NULL,
|
||||
"token_expires_at" BIGINT NOT NULL,
|
||||
"refresh_token_expires_at" BIGINT NOT NULL,
|
||||
"nonce" TEXT NOT NULL DEFAULT '',
|
||||
"userinfo_json" TEXT NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,46 @@
|
||||
DROP TABLE IF EXISTS "oidc_sessions";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "oidc_codes" (
|
||||
"sub" TEXT NOT NULL UNIQUE,
|
||||
"code_hash" TEXT NOT NULL PRIMARY KEY UNIQUE,
|
||||
"scope" TEXT NOT NULL,
|
||||
"redirect_uri" TEXT NOT NULL,
|
||||
"client_id" TEXT NOT NULL,
|
||||
"expires_at" INTEGER NOT NULL,
|
||||
"nonce" TEXT DEFAULT "",
|
||||
"code_challenge" TEXT DEFAULT ""
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "oidc_tokens" (
|
||||
"sub" TEXT NOT NULL UNIQUE,
|
||||
"access_token_hash" TEXT NOT NULL PRIMARY KEY UNIQUE,
|
||||
"refresh_token_hash" TEXT NOT NULL,
|
||||
"code_hash" TEXT NOT NULL,
|
||||
"scope" TEXT NOT NULL,
|
||||
"client_id" TEXT NOT NULL,
|
||||
"token_expires_at" INTEGER NOT NULL,
|
||||
"refresh_token_expires_at" INTEGER NOT NULL,
|
||||
"nonce" TEXT DEFAULT ""
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "oidc_userinfo" (
|
||||
"sub" TEXT NOT NULL UNIQUE PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"preferred_username" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"groups" TEXT NOT NULL,
|
||||
"updated_at" INTEGER NOT NULL,
|
||||
"given_name" TEXT NOT NULL,
|
||||
"family_name" TEXT NOT NULL,
|
||||
"middle_name" TEXT NOT NULL,
|
||||
"nickname" TEXT NOT NULL,
|
||||
"profile" TEXT NOT NULL,
|
||||
"picture" TEXT NOT NULL,
|
||||
"website" TEXT NOT NULL,
|
||||
"gender" TEXT NOT NULL,
|
||||
"birthdate" TEXT NOT NULL,
|
||||
"zoneinfo" TEXT NOT NULL,
|
||||
"locale" TEXT NOT NULL,
|
||||
"phone_number" TEXT NOT NULL,
|
||||
"address" TEXT NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
This migration will nuke the entire setup of OIDC sessions and merge everything
|
||||
into one table.
|
||||
*/
|
||||
|
||||
/*
|
||||
Drop all the old tables. Yes, we will log out all OIDC users, but not really a big deal
|
||||
*/
|
||||
|
||||
DROP TABLE IF EXISTS "oidc_tokens";
|
||||
DROP TABLE IF EXISTS "oidc_userinfo";
|
||||
DROP TABLE IF EXISTS "oidc_codes";
|
||||
|
||||
/*
|
||||
Create a new simple OIDC sessions table that will hold tokens + userinfo.
|
||||
*/
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "oidc_sessions" (
|
||||
"sub" TEXT NOT NULL UNIQUE PRIMARY KEY,
|
||||
"access_token_hash" TEXT NOT NULL UNIQUE,
|
||||
"refresh_token_hash" TEXT NOT NULL UNIQUE,
|
||||
"scope" TEXT NOT NULL,
|
||||
"client_id" TEXT NOT NULL,
|
||||
"token_expires_at" INTEGER NOT NULL,
|
||||
"refresh_token_expires_at" INTEGER NOT NULL,
|
||||
"nonce" TEXT DEFAULT "",
|
||||
"userinfo_json" TEXT NOT NULL
|
||||
);
|
||||
@@ -11,13 +11,15 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/steveiliop56/ding"
|
||||
"go.uber.org/dig"
|
||||
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||
@@ -26,6 +28,12 @@ import (
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
)
|
||||
|
||||
// Shutdown order for go routines
|
||||
// 1. Janitor routines (e.g. database cleanup, heartbeat) - ding.RingMinor
|
||||
// 2. HTTP server listeners - ding.RingNormal
|
||||
// 3. Networking layers, user and label providers (e.g. ailscale service, kubernetes service) - ding.RingMajor
|
||||
// 4. Database connection - ding.RingCritical
|
||||
|
||||
type Services struct {
|
||||
accessControlService *service.AccessControlsService
|
||||
authService *service.AuthService
|
||||
@@ -48,8 +56,8 @@ type BootstrapApp struct {
|
||||
queries repository.Store
|
||||
router *gin.Engine
|
||||
db *sql.DB
|
||||
wg sync.WaitGroup
|
||||
listeners []Listener
|
||||
ding *ding.Ding
|
||||
dig *dig.Container
|
||||
}
|
||||
|
||||
func NewBootstrapApp(config model.Config) *BootstrapApp {
|
||||
@@ -64,6 +72,14 @@ func (app *BootstrapApp) Setup() error {
|
||||
app.ctx = ctx
|
||||
app.cancel = cancel
|
||||
|
||||
// create the dig container
|
||||
c := dig.New()
|
||||
app.dig = c
|
||||
|
||||
// create a ding instance
|
||||
dg := ding.New(ctx)
|
||||
app.ding = dg
|
||||
|
||||
// setup logger
|
||||
log := logger.NewLogger().WithConfig(app.config.Log)
|
||||
log.Init()
|
||||
@@ -82,8 +98,7 @@ func (app *BootstrapApp) Setup() error {
|
||||
return fmt.Errorf("failed to parse app url: %w", err)
|
||||
}
|
||||
|
||||
app.runtime.AppURL = appUrl.Scheme + "://" + appUrl.Host
|
||||
app.runtime.TrustedDomains = append(app.runtime.TrustedDomains, app.runtime.AppURL)
|
||||
app.runtime.AppURL = strings.ToLower(appUrl.Scheme + "://" + appUrl.Host)
|
||||
|
||||
// validate session config
|
||||
if app.config.Auth.SessionMaxLifetime != 0 && app.config.Auth.SessionMaxLifetime < app.config.Auth.SessionExpiry {
|
||||
@@ -97,7 +112,12 @@ func (app *BootstrapApp) Setup() error {
|
||||
return fmt.Errorf("failed to load users: %w", err)
|
||||
}
|
||||
|
||||
if users != nil {
|
||||
app.runtime.LocalUsers = *users
|
||||
} else {
|
||||
log.App.Debug().Msg("No local users found, local authentication will not be available")
|
||||
app.runtime.LocalUsers = []model.LocalUser{}
|
||||
}
|
||||
|
||||
// load oauth whitelist
|
||||
oauthWhitelist, err := utils.GetStringList(app.config.OAuth.Whitelist, app.config.OAuth.WhitelistFile)
|
||||
@@ -112,19 +132,21 @@ func (app *BootstrapApp) Setup() error {
|
||||
app.runtime.OAuthProviders = app.config.OAuth.Providers
|
||||
|
||||
for id, provider := range app.runtime.OAuthProviders {
|
||||
if slices.Contains(model.ReservedProviderNames, id) {
|
||||
return fmt.Errorf("provider id %s is reserved and cannot be used", id)
|
||||
}
|
||||
|
||||
providerWhitelist, err := utils.GetStringList(provider.Whitelist, provider.WhitelistFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load oauth whitelist for provider %s: %w", id, err)
|
||||
}
|
||||
|
||||
provider.Whitelist = providerWhitelist
|
||||
|
||||
secret := utils.GetSecret(provider.ClientSecret, provider.ClientSecretFile)
|
||||
provider.ClientSecret = secret
|
||||
provider.ClientSecretFile = ""
|
||||
|
||||
if provider.RedirectURL == "" {
|
||||
provider.RedirectURL = app.runtime.AppURL + "/api/oauth/callback/" + id
|
||||
}
|
||||
|
||||
app.runtime.OAuthProviders[id] = provider
|
||||
}
|
||||
|
||||
// set presets for built-in providers
|
||||
for id, provider := range app.runtime.OAuthProviders {
|
||||
if provider.Name == "" {
|
||||
if name, ok := model.OverrideProviders[id]; ok {
|
||||
provider.Name = name
|
||||
@@ -132,24 +154,16 @@ func (app *BootstrapApp) Setup() error {
|
||||
provider.Name = utils.Capitalize(id)
|
||||
}
|
||||
}
|
||||
|
||||
app.runtime.OAuthProviders[id] = provider
|
||||
}
|
||||
|
||||
// setup oidc clients
|
||||
for id, client := range app.config.OIDC.Clients {
|
||||
client.ID = id
|
||||
app.runtime.OIDCClients = append(app.runtime.OIDCClients, client)
|
||||
}
|
||||
|
||||
// cookie domain
|
||||
cookieDomainResolver := utils.GetCookieDomain
|
||||
|
||||
if !app.config.Auth.SubdomainsEnabled {
|
||||
app.log.App.Warn().Msg("Subdomains are disabled, using standalone cookie domain resolver which will not work with subdomains")
|
||||
cookieDomainResolver = utils.GetStandaloneCookieDomain
|
||||
app.log.App.Warn().Msg("Subdomains are disabled, cookies will be set for the current domain only")
|
||||
}
|
||||
|
||||
cookieDomain, err := cookieDomainResolver(app.runtime.AppURL)
|
||||
cookieDomain, err := utils.GetCookieDomain(app.runtime.AppURL, app.config.Auth.SubdomainsEnabled)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get cookie domain: %w", err)
|
||||
@@ -163,8 +177,7 @@ func (app *BootstrapApp) Setup() error {
|
||||
cookieId := strings.Split(app.runtime.UUID, "-")[0] // first 8 characters of the uuid should be good enough
|
||||
|
||||
app.runtime.SessionCookieName = fmt.Sprintf("%s-%s", model.SessionCookieName, cookieId)
|
||||
app.runtime.CSRFCookieName = fmt.Sprintf("%s-%s", model.CSRFCookieName, cookieId)
|
||||
app.runtime.RedirectCookieName = fmt.Sprintf("%s-%s", model.RedirectCookieName, cookieId)
|
||||
app.runtime.ScopeCookieName = fmt.Sprintf("%s-%s", model.OIDCScopeCookieName, cookieId)
|
||||
app.runtime.OAuthSessionCookieName = fmt.Sprintf("%s-%s", model.OAuthSessionCookieName, cookieId)
|
||||
|
||||
// database
|
||||
@@ -174,19 +187,48 @@ func (app *BootstrapApp) Setup() error {
|
||||
return fmt.Errorf("failed to setup database: %w", err)
|
||||
}
|
||||
|
||||
// after this point, we start initializing dependencies so it's a good time to setup a defer
|
||||
// to ensure that resources are cleaned up properly in case of an error during initialization
|
||||
defer func() {
|
||||
app.cancel()
|
||||
app.wg.Wait()
|
||||
if app.db != nil {
|
||||
app.db.Close()
|
||||
app.ding.Go(func(ctx context.Context) {
|
||||
<-ctx.Done()
|
||||
app.log.App.Debug().Msg("Shutting down database connection")
|
||||
if app.db == nil {
|
||||
// using memory store, no db instance
|
||||
return
|
||||
}
|
||||
}()
|
||||
if err := app.db.Close(); err != nil {
|
||||
app.log.App.Error().Err(err).Msg("Failed to close database connection")
|
||||
}
|
||||
}, ding.RingCritical)
|
||||
|
||||
// store
|
||||
app.queries = store
|
||||
|
||||
// provide basic utilities to container
|
||||
type utilityProvider struct {
|
||||
dig.Out
|
||||
|
||||
Log *logger.Logger
|
||||
Config *model.Config
|
||||
Runtime *model.RuntimeConfig
|
||||
Ding *ding.Ding
|
||||
Ctx context.Context
|
||||
Queries repository.Store
|
||||
}
|
||||
|
||||
err = app.dig.Provide(func() utilityProvider {
|
||||
return utilityProvider{
|
||||
Log: app.log,
|
||||
Config: &app.config,
|
||||
Runtime: &app.runtime,
|
||||
Ding: app.ding,
|
||||
Ctx: app.ctx,
|
||||
Queries: app.queries,
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to provide utilities to container: %w", err)
|
||||
}
|
||||
|
||||
// services
|
||||
err = app.setupServices()
|
||||
|
||||
@@ -235,9 +277,22 @@ func (app *BootstrapApp) Setup() error {
|
||||
|
||||
app.runtime.ConfiguredProviders = configuredProviders
|
||||
|
||||
// throw in tailscale if it's configured just before setting up the controllers
|
||||
if app.services.tailscaleService != nil {
|
||||
app.runtime.TrustedDomains = append(app.runtime.TrustedDomains, "https://"+app.services.tailscaleService.GetHostname())
|
||||
// force an update of the redirect urls for all oauth providers, if they are empty
|
||||
services := app.services.oauthBrokerService.GetConfiguredServices()
|
||||
|
||||
for _, service := range services {
|
||||
oauthService, ok := app.services.oauthBrokerService.GetService(service)
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to get oauth service for provider %s", service)
|
||||
}
|
||||
|
||||
providerConfig := oauthService.GetConfig()
|
||||
|
||||
if providerConfig.RedirectURL == "" {
|
||||
providerConfig.RedirectURL = app.runtime.AppURL + "/api/oauth/callback/" + service
|
||||
oauthService.UpdateConfig(providerConfig)
|
||||
}
|
||||
}
|
||||
|
||||
// setup router
|
||||
@@ -249,32 +304,33 @@ func (app *BootstrapApp) Setup() error {
|
||||
|
||||
// start db cleanup routine
|
||||
app.log.App.Debug().Msg("Starting database cleanup routine")
|
||||
app.wg.Go(app.dbCleanupRoutine)
|
||||
app.ding.Go(app.dbCleanupRoutine, ding.RingMinor)
|
||||
|
||||
// if analytics are not disabled, start heartbeat
|
||||
if app.config.Analytics.Enabled {
|
||||
app.log.App.Debug().Msg("Starting heartbeat routine")
|
||||
app.wg.Go(app.heartbeatRoutine)
|
||||
app.ding.Go(app.heartbeatRoutine, ding.RingMinor)
|
||||
}
|
||||
|
||||
// setup listeners
|
||||
app.listeners = app.calculateListenerPolicy()
|
||||
|
||||
if app.config.Server.ConcurrentListenersEnabled {
|
||||
app.log.App.Info().Msg("Concurrent listeners enabled, will run on all available listeners")
|
||||
}
|
||||
|
||||
// run listeners
|
||||
lec, err := app.runListeners()
|
||||
// get listener
|
||||
listenerFunc, err := app.getListenerFunc()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run listeners: %w", err)
|
||||
return fmt.Errorf("failed to get listener function: %w", err)
|
||||
}
|
||||
|
||||
// run listener
|
||||
lec := make(chan error, 1)
|
||||
|
||||
app.ding.Go(func(ctx context.Context) {
|
||||
lec <- listenerFunc(ctx)
|
||||
}, ding.RingNormal)
|
||||
|
||||
// monitor cancellation and server errors
|
||||
for {
|
||||
select {
|
||||
case <-app.ctx.Done():
|
||||
app.ding.Wait()
|
||||
app.log.App.Info().Msg("Oh, it's time for me to go, bye!")
|
||||
return nil
|
||||
case err := <-lec:
|
||||
@@ -285,7 +341,7 @@ func (app *BootstrapApp) Setup() error {
|
||||
}
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) heartbeatRoutine() {
|
||||
func (app *BootstrapApp) heartbeatRoutine(ctx context.Context) {
|
||||
ticker := time.NewTicker(time.Duration(12) * time.Hour)
|
||||
defer ticker.Stop()
|
||||
|
||||
@@ -338,7 +394,7 @@ func (app *BootstrapApp) heartbeatRoutine() {
|
||||
if res.StatusCode != 200 && res.StatusCode != 201 {
|
||||
app.log.App.Debug().Str("status", res.Status).Msg("Heartbeat returned non-200/201 status")
|
||||
}
|
||||
case <-app.ctx.Done():
|
||||
case <-ctx.Done():
|
||||
app.log.App.Debug().Msg("Stopping heartbeat routine")
|
||||
ticker.Stop()
|
||||
return
|
||||
@@ -346,7 +402,7 @@ func (app *BootstrapApp) heartbeatRoutine() {
|
||||
}
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) dbCleanupRoutine() {
|
||||
func (app *BootstrapApp) dbCleanupRoutine(ctx context.Context) {
|
||||
ticker := time.NewTicker(time.Duration(30) * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
@@ -355,14 +411,14 @@ func (app *BootstrapApp) dbCleanupRoutine() {
|
||||
case <-ticker.C:
|
||||
app.log.App.Debug().Msg("Running database cleanup")
|
||||
|
||||
err := app.queries.DeleteExpiredSessions(app.ctx, time.Now().Unix())
|
||||
err := app.queries.DeleteExpiredSessions(ctx, time.Now().Unix())
|
||||
|
||||
if err != nil {
|
||||
app.log.App.Error().Err(err).Msg("Failed to delete expired sessions")
|
||||
}
|
||||
|
||||
app.log.App.Debug().Msg("Database cleanup completed")
|
||||
case <-app.ctx.Done():
|
||||
case <-ctx.Done():
|
||||
app.log.App.Debug().Msg("Stopping database cleanup routine")
|
||||
ticker.Stop()
|
||||
return
|
||||
|
||||
@@ -12,18 +12,11 @@ import (
|
||||
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||
"github.com/tinyauthapp/tinyauth/internal/middleware"
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
"go.uber.org/dig"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Listener int
|
||||
|
||||
const (
|
||||
ListenerHTTP Listener = iota
|
||||
ListenerUnix
|
||||
ListenerTailscale
|
||||
)
|
||||
|
||||
func (app *BootstrapApp) setupRouter() error {
|
||||
// we don't want gin debug mode
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
@@ -39,109 +32,114 @@ func (app *BootstrapApp) setupRouter() error {
|
||||
}
|
||||
}
|
||||
|
||||
contextMiddleware := middleware.NewContextMiddleware(app.log, app.runtime, app.services.authService, app.services.oauthBrokerService, app.services.tailscaleService)
|
||||
engine.Use(contextMiddleware.Middleware())
|
||||
|
||||
uiMiddleware, err := middleware.NewUIMiddleware()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize UI middleware: %w", err)
|
||||
middlewareProvideFor := []any{
|
||||
middleware.NewContextMiddleware,
|
||||
middleware.NewUIMiddleware,
|
||||
middleware.NewZerologMiddleware,
|
||||
}
|
||||
|
||||
engine.Use(uiMiddleware.Middleware())
|
||||
for _, provider := range middlewareProvideFor {
|
||||
err := app.dig.Provide(provider)
|
||||
|
||||
zerologMiddleware := middleware.NewZerologMiddleware(app.log)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to provide middleware: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
engine.Use(zerologMiddleware.Middleware())
|
||||
type middlewareInput struct {
|
||||
dig.In
|
||||
|
||||
apiRouter := engine.Group("/api")
|
||||
ContextMiddleware *middleware.ContextMiddleware
|
||||
UIMiddleware *middleware.UIMiddleware
|
||||
ZerologMiddleware *middleware.ZerologMiddleware
|
||||
}
|
||||
|
||||
controller.NewContextController(app.log, app.config, app.runtime, apiRouter)
|
||||
controller.NewOAuthController(app.log, app.config, app.runtime, apiRouter, app.services.authService)
|
||||
controller.NewOIDCController(app.log, app.services.oidcService, app.runtime, apiRouter)
|
||||
controller.NewProxyController(app.log, app.runtime, apiRouter, app.services.accessControlService, app.services.authService, app.services.policyEngine)
|
||||
controller.NewUserController(app.log, app.runtime, apiRouter, app.services.authService)
|
||||
controller.NewResourcesController(app.config, &engine.RouterGroup)
|
||||
controller.NewHealthController(apiRouter)
|
||||
controller.NewWellKnownController(app.services.oidcService, &engine.RouterGroup)
|
||||
err := app.dig.Invoke(func(mi middlewareInput) {
|
||||
engine.Use(mi.ContextMiddleware.Middleware())
|
||||
engine.Use(mi.UIMiddleware.Middleware())
|
||||
engine.Use(mi.ZerologMiddleware.Middleware())
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to invoke middleware: %w", err)
|
||||
}
|
||||
|
||||
err = app.dig.Provide(func() *gin.RouterGroup {
|
||||
return &engine.RouterGroup
|
||||
}, dig.Name("mainRouterGroup"))
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to provide main router group: %w", err)
|
||||
}
|
||||
|
||||
err = app.dig.Provide(func() *gin.RouterGroup {
|
||||
return engine.Group("/api")
|
||||
}, dig.Name("apiRouterGroup"))
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to provide api router group: %w", err)
|
||||
}
|
||||
|
||||
controllerProvideFor := []any{
|
||||
controller.NewContextController,
|
||||
controller.NewOAuthController,
|
||||
controller.NewOIDCController,
|
||||
controller.NewProxyController,
|
||||
controller.NewUserController,
|
||||
controller.NewResourcesController,
|
||||
controller.NewHealthController,
|
||||
controller.NewWellKnownController,
|
||||
}
|
||||
|
||||
for _, provider := range controllerProvideFor {
|
||||
err := app.dig.Provide(provider)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to provide controller: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
type controllerInput struct {
|
||||
dig.In
|
||||
|
||||
ContextController *controller.ContextController
|
||||
OAuthController *controller.OAuthController
|
||||
OIDCController *controller.OIDCController
|
||||
ProxyController *controller.ProxyController
|
||||
UserController *controller.UserController
|
||||
ResourcesController *controller.ResourcesController
|
||||
HealthController *controller.HealthController
|
||||
WellKnownController *controller.WellKnownController
|
||||
}
|
||||
|
||||
// force dig to build all controllers and register their routes
|
||||
err = app.dig.Invoke(func(ci controllerInput) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to invoke controllers: %w", err)
|
||||
}
|
||||
|
||||
app.router = engine
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) runListeners() (chan error, error) {
|
||||
// lec -> listener error channel
|
||||
lec := make(chan error, len(app.listeners))
|
||||
|
||||
for _, listenerType := range app.listeners {
|
||||
listenerFunc, err := app.listenerFromType(listenerType)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get listener function: %w", err)
|
||||
}
|
||||
|
||||
app.wg.Go(func() {
|
||||
lec <- listenerFunc()
|
||||
})
|
||||
}
|
||||
|
||||
return lec, nil
|
||||
}
|
||||
|
||||
// The way we calculate listeners is as follows:
|
||||
// If concurrent listeners are disabled, we pick the first available listener, so:
|
||||
// 1. If tailscale is enabled, we use tailscale
|
||||
// 2. If socket path is configured, we use unix socket
|
||||
// 3. Finally if none is configured we use http
|
||||
// If concurrent listeners are enabled, we add all available listeners in the following order
|
||||
func (app *BootstrapApp) calculateListenerPolicy() []Listener {
|
||||
l := []Listener{}
|
||||
|
||||
if !app.config.Server.ConcurrentListenersEnabled {
|
||||
if app.services.tailscaleService != nil {
|
||||
l = append(l, ListenerTailscale)
|
||||
return l
|
||||
}
|
||||
|
||||
// Top down
|
||||
// 1. Unix socket (if server.socketPath)
|
||||
// 2. HTTP - default
|
||||
func (app *BootstrapApp) getListenerFunc() (func(ctx context.Context) error, error) {
|
||||
if app.config.Server.SocketPath != "" {
|
||||
l = append(l, ListenerUnix)
|
||||
return l
|
||||
}
|
||||
|
||||
l = append(l, ListenerHTTP)
|
||||
return l
|
||||
}
|
||||
|
||||
if app.config.Server.SocketPath != "" {
|
||||
l = append(l, ListenerUnix)
|
||||
}
|
||||
|
||||
if app.services.tailscaleService != nil {
|
||||
l = append(l, ListenerTailscale)
|
||||
}
|
||||
|
||||
l = append(l, ListenerHTTP)
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) listenerFromType(listenerType Listener) (func() error, error) {
|
||||
switch listenerType {
|
||||
case ListenerHTTP:
|
||||
return app.serveHTTP, nil
|
||||
case ListenerUnix:
|
||||
return app.serveUnix, nil
|
||||
case ListenerTailscale:
|
||||
return app.serveTailscale, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid listener type: %d", listenerType)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) serveHTTP() error {
|
||||
return app.serveHTTP, nil
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) serveHTTP(ctx context.Context) error {
|
||||
address := fmt.Sprintf("%s:%d", app.config.Server.Address, app.config.Server.Port)
|
||||
|
||||
app.log.App.Info().Msgf("Starting server on %s", address)
|
||||
app.log.App.Info().Msgf("Starting server on http://%s", address)
|
||||
|
||||
listener, err := net.Listen("tcp", address)
|
||||
|
||||
@@ -154,10 +152,10 @@ func (app *BootstrapApp) serveHTTP() error {
|
||||
Handler: app.router.Handler(),
|
||||
}
|
||||
|
||||
return app.serve(listener, server, "http")
|
||||
return app.serve(listener, server, ctx, "http")
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) serveUnix() error {
|
||||
func (app *BootstrapApp) serveUnix(ctx context.Context) error {
|
||||
_, err := os.Stat(app.config.Server.SocketPath)
|
||||
|
||||
if err == nil {
|
||||
@@ -181,43 +179,23 @@ func (app *BootstrapApp) serveUnix() error {
|
||||
Handler: app.router.Handler(),
|
||||
}
|
||||
|
||||
return app.serve(listener, server, "unix socket")
|
||||
return app.serve(listener, server, ctx, "unix socket")
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) serveTailscale() error {
|
||||
app.log.App.Info().Msgf("Starting Tailscale server on %s", fmt.Sprintf("https://%s", app.services.tailscaleService.GetHostname()))
|
||||
|
||||
listener, err := app.services.tailscaleService.CreateListener()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create tailscale listener: %w", err)
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Handler: app.router.Handler(),
|
||||
}
|
||||
|
||||
return app.serve(listener, server, "tailscale")
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) serve(listener net.Listener, server *http.Server, name string) error {
|
||||
func (app *BootstrapApp) serve(listener net.Listener, server *http.Server, ctx context.Context, name string) error {
|
||||
shutdown := func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), model.GracefulShutdownTimeout*time.Second)
|
||||
// we use a new context for the shutdown since the main one is cancelled
|
||||
sctx, cancel := context.WithTimeout(context.Background(), model.GracefulShutdownTimeout*time.Second)
|
||||
defer cancel()
|
||||
err := server.Shutdown(ctx)
|
||||
if err != nil &&
|
||||
// With tailscale, the goroutine for shutting down the tailscale connection
|
||||
// runs first and causes the connection the tailscale listener is running on to close
|
||||
// first so, the shutdown fails
|
||||
// TODO: add priority to the goroutine shutdowns
|
||||
!errors.Is(err, net.ErrClosed) {
|
||||
err := server.Shutdown(sctx)
|
||||
if err != nil {
|
||||
app.log.App.Error().Err(err).Msgf("Failed to shutdown %s listener gracefully", name)
|
||||
}
|
||||
listener.Close()
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-app.ctx.Done()
|
||||
<-ctx.Done()
|
||||
app.log.App.Debug().Msgf("Shutting down %s listener", name)
|
||||
shutdown()
|
||||
}()
|
||||
@@ -225,7 +203,6 @@ func (app *BootstrapApp) serve(listener net.Listener, server *http.Server, name
|
||||
err := server.Serve(listener)
|
||||
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
shutdown()
|
||||
return fmt.Errorf("failed to start %s listener: %w", name, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,58 +2,70 @@ package bootstrap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"os"
|
||||
|
||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
func (app *BootstrapApp) setupServices() error {
|
||||
ldapService, err := service.NewLdapService(app.log, app.config, app.ctx, &app.wg)
|
||||
err := app.setupPolicyEngine()
|
||||
|
||||
if err != nil {
|
||||
app.log.App.Warn().Err(err).Msg("Failed to initialize LDAP connection, will continue without it")
|
||||
return fmt.Errorf("failed to setup policy engine: %w", err)
|
||||
}
|
||||
|
||||
app.services.ldapService = ldapService
|
||||
|
||||
labelProvider, err := app.getLabelProvider()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize label provider: %w", err)
|
||||
return fmt.Errorf("failed to get label provider: %w", err)
|
||||
}
|
||||
|
||||
tailscaleService, err := service.NewTailscaleService(app.log, app.config, app.ctx, &app.wg)
|
||||
serviceProvideFor := []any{
|
||||
func() service.LabelProvider {
|
||||
return labelProvider
|
||||
},
|
||||
service.NewLdapService,
|
||||
service.NewTailscaleService,
|
||||
service.NewAccessControlsService,
|
||||
service.NewOAuthBrokerService,
|
||||
service.NewAuthService,
|
||||
service.NewOIDCService,
|
||||
}
|
||||
|
||||
for _, provider := range serviceProvideFor {
|
||||
err = app.dig.Provide(provider)
|
||||
|
||||
if err != nil {
|
||||
app.log.App.Warn().Err(err).Msg("Failed to initialize Tailscale connection, will continue without it")
|
||||
return fmt.Errorf("failed to provide service: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
app.services.tailscaleService = tailscaleService
|
||||
type svcInput struct {
|
||||
dig.In
|
||||
|
||||
accessControlsService := service.NewAccessControlsService(app.log, app.config, &labelProvider)
|
||||
app.services.accessControlService = accessControlsService
|
||||
AccessControlService *service.AccessControlsService
|
||||
AuthService *service.AuthService
|
||||
LDAPService *service.LdapService
|
||||
OAuthBrokerService *service.OAuthBrokerService
|
||||
OIDCService *service.OIDCService
|
||||
TailscaleService *service.TailscaleService
|
||||
}
|
||||
|
||||
err = app.setupPolicyEngine()
|
||||
err = app.dig.Invoke(func(i svcInput) error {
|
||||
app.services.accessControlService = i.AccessControlService
|
||||
app.services.authService = i.AuthService
|
||||
app.services.ldapService = i.LDAPService
|
||||
app.services.oauthBrokerService = i.OAuthBrokerService
|
||||
app.services.oidcService = i.OIDCService
|
||||
app.services.tailscaleService = i.TailscaleService
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize policy engine: %w", err)
|
||||
return fmt.Errorf("failed to invoke services: %w", err)
|
||||
}
|
||||
|
||||
oauthBrokerService := service.NewOAuthBrokerService(app.log, app.runtime.OAuthProviders, app.ctx)
|
||||
app.services.oauthBrokerService = oauthBrokerService
|
||||
|
||||
authService := service.NewAuthService(app.log, app.config, app.runtime, app.ctx, &app.wg, app.services.ldapService, app.queries, app.services.oauthBrokerService, app.services.tailscaleService)
|
||||
app.services.authService = authService
|
||||
|
||||
oidcService, err := service.NewOIDCService(app.log, app.config, app.runtime, app.queries, app.ctx, &app.wg)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize oidc service: %w", err)
|
||||
}
|
||||
|
||||
app.services.oidcService = oidcService
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -70,45 +82,71 @@ func (app *BootstrapApp) getLabelProvider() (service.LabelProvider, error) {
|
||||
if useKubernetes {
|
||||
app.log.App.Debug().Msg("Using Kubernetes label provider")
|
||||
|
||||
kubernetesService, err := service.NewKubernetesService(app.log, app.ctx, &app.wg)
|
||||
err := app.dig.Provide(service.NewKubernetesService)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize kubernetes service: %w", err)
|
||||
return nil, fmt.Errorf("failed to provide kubernetes service: %w", err)
|
||||
}
|
||||
|
||||
app.services.kubernetesService = kubernetesService
|
||||
return kubernetesService, nil
|
||||
err = app.dig.Invoke(func(k *service.KubernetesService) error {
|
||||
app.services.kubernetesService = k
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to invoke kubernetes service: %w", err)
|
||||
}
|
||||
|
||||
// Kubernetes will fail to initialize with an error if it cannot connect to the cluster
|
||||
// but just to be safe, we check if the service is nil and log a warning if it is
|
||||
if app.services.kubernetesService == nil {
|
||||
if app.config.LabelProvider == "kubernetes" {
|
||||
app.log.App.Warn().Msg("Kubernetes label provider selected but Kubernetes is not available, will continue without it")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return app.services.kubernetesService, nil
|
||||
}
|
||||
|
||||
app.log.App.Debug().Msg("Using Docker label provider")
|
||||
|
||||
dockerService, err := service.NewDockerService(app.log, app.ctx, &app.wg)
|
||||
err := app.dig.Provide(service.NewDockerService)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize docker service: %w", err)
|
||||
return nil, fmt.Errorf("failed to provide docker service: %w", err)
|
||||
}
|
||||
|
||||
if dockerService == nil {
|
||||
err = app.dig.Invoke(func(d *service.DockerService) error {
|
||||
app.services.dockerService = d
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to invoke docker service: %w", err)
|
||||
}
|
||||
|
||||
if app.services.dockerService == nil {
|
||||
if app.config.LabelProvider == "docker" {
|
||||
app.log.App.Warn().Msg("Docker label provider selected but Docker is not available, will continue without it")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
app.services.dockerService = dockerService
|
||||
return dockerService, nil
|
||||
return app.services.dockerService, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid label provider: %s", app.config.LabelProvider)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) setupPolicyEngine() error {
|
||||
policyEngine, err := service.NewPolicyEngine(app.config, app.log)
|
||||
err := app.dig.Provide(service.NewPolicyEngine)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize policy engine: %w", err)
|
||||
return fmt.Errorf("failed to create policy engine: %w", err)
|
||||
}
|
||||
|
||||
err = app.dig.Invoke(func(policyEngine *service.PolicyEngine) error {
|
||||
policyEngine.RegisterRule(service.RuleUserAllowed, &service.UserAllowedRule{
|
||||
Log: app.log,
|
||||
})
|
||||
@@ -127,8 +165,10 @@ func (app *BootstrapApp) setupPolicyEngine() error {
|
||||
})
|
||||
policyEngine.RegisterRule(service.RuleIPBypassed, &service.IPBypassedRule{
|
||||
Log: app.log,
|
||||
Config: app.config,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
|
||||
app.services.policyEngine = policyEngine
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
"go.uber.org/dig"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -59,7 +62,7 @@ type ACRUI struct {
|
||||
type ACRApp struct {
|
||||
AppURL string `json:"appUrl"`
|
||||
CookieDomain string `json:"cookieDomain"`
|
||||
TrustedDomains []string `json:"trustedDomains"`
|
||||
SubdomainsEnabled bool `json:"subdomainsEnabled"`
|
||||
}
|
||||
|
||||
type AppContextResponse struct {
|
||||
@@ -71,29 +74,33 @@ type AppContextResponse struct {
|
||||
App ACRApp `json:"app"`
|
||||
}
|
||||
|
||||
type ContextControllerInput struct {
|
||||
dig.In
|
||||
|
||||
Log *logger.Logger
|
||||
Config *model.Config
|
||||
Runtime *model.RuntimeConfig
|
||||
RouterGroup *gin.RouterGroup `name:"apiRouterGroup"`
|
||||
}
|
||||
|
||||
type ContextController struct {
|
||||
log *logger.Logger
|
||||
config model.Config
|
||||
runtime model.RuntimeConfig
|
||||
config *model.Config
|
||||
runtime *model.RuntimeConfig
|
||||
}
|
||||
|
||||
func NewContextController(
|
||||
log *logger.Logger,
|
||||
config model.Config,
|
||||
runtimeConfig model.RuntimeConfig,
|
||||
router *gin.RouterGroup,
|
||||
) *ContextController {
|
||||
func NewContextController(i ContextControllerInput) *ContextController {
|
||||
controller := &ContextController{
|
||||
log: log,
|
||||
config: config,
|
||||
runtime: runtimeConfig,
|
||||
log: i.Log,
|
||||
config: i.Config,
|
||||
runtime: i.Runtime,
|
||||
}
|
||||
|
||||
if !config.UI.WarningsEnabled {
|
||||
log.App.Warn().Msg("UI warnings are disabled. This may lead to security issues if you are not careful. Make sure to enable warnings in production environments.")
|
||||
if !i.Config.UI.WarningsEnabled {
|
||||
i.Log.App.Warn().Msg("UI warnings are disabled. This may lead to security issues if you are not careful. Make sure to enable warnings in production environments.")
|
||||
}
|
||||
|
||||
contextGroup := router.Group("/context")
|
||||
contextGroup := i.RouterGroup.Group("/context")
|
||||
contextGroup.GET("/user", controller.userContextHandler)
|
||||
contextGroup.GET("/app", controller.appContextHandler)
|
||||
|
||||
@@ -104,7 +111,9 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
|
||||
context, err := new(model.UserContext).NewFromGin(c)
|
||||
|
||||
if err != nil {
|
||||
if !errors.Is(err, model.ErrUserContextNotFound) {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to create user context from request")
|
||||
}
|
||||
c.JSON(200, UserContextResponse{
|
||||
Status: 401,
|
||||
Message: "Unauthorized",
|
||||
@@ -157,7 +166,7 @@ func (controller *ContextController) appContextHandler(c *gin.Context) {
|
||||
App: ACRApp{
|
||||
AppURL: controller.runtime.AppURL,
|
||||
CookieDomain: controller.runtime.CookieDomain,
|
||||
TrustedDomains: controller.runtime.TrustedDomains,
|
||||
SubdomainsEnabled: controller.config.Auth.SubdomainsEnabled,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package controller_test
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
"github.com/tinyauthapp/tinyauth/internal/test"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||
@@ -33,25 +32,25 @@ func TestContextController(t *testing.T) {
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
path: "/api/context/app",
|
||||
expected: func() string {
|
||||
expectedAppContextResponse := controller.AppContextResponse{
|
||||
expectedAppContextResponse := AppContextResponse{
|
||||
Status: 200,
|
||||
Message: "Success",
|
||||
Auth: controller.ACRAuth{
|
||||
Auth: ACRAuth{
|
||||
Providers: runtime.ConfiguredProviders,
|
||||
},
|
||||
OAuth: controller.ACROAuth{
|
||||
OAuth: ACROAuth{
|
||||
AutoRedirect: cfg.OAuth.AutoRedirect,
|
||||
},
|
||||
UI: controller.ACRUI{
|
||||
UI: ACRUI{
|
||||
Title: cfg.UI.Title,
|
||||
ForgotPasswordMessage: cfg.UI.ForgotPasswordMessage,
|
||||
BackgroundImage: cfg.UI.BackgroundImage,
|
||||
WarningsEnabled: cfg.UI.WarningsEnabled,
|
||||
},
|
||||
App: controller.ACRApp{
|
||||
App: ACRApp{
|
||||
AppURL: runtime.AppURL,
|
||||
CookieDomain: runtime.CookieDomain,
|
||||
TrustedDomains: runtime.TrustedDomains,
|
||||
SubdomainsEnabled: cfg.Auth.SubdomainsEnabled,
|
||||
},
|
||||
}
|
||||
bytes, err := json.Marshal(expectedAppContextResponse)
|
||||
@@ -64,7 +63,7 @@ func TestContextController(t *testing.T) {
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
path: "/api/context/user",
|
||||
expected: func() string {
|
||||
expectedUserContextResponse := controller.UserContextResponse{
|
||||
expectedUserContextResponse := UserContextResponse{
|
||||
Status: 401,
|
||||
Message: "Unauthorized",
|
||||
}
|
||||
@@ -92,10 +91,10 @@ func TestContextController(t *testing.T) {
|
||||
},
|
||||
path: "/api/context/user",
|
||||
expected: func() string {
|
||||
expectedUserContextResponse := controller.UserContextResponse{
|
||||
expectedUserContextResponse := UserContextResponse{
|
||||
Status: 200,
|
||||
Message: "Success",
|
||||
Auth: controller.UCRAuth{
|
||||
Auth: UCRAuth{
|
||||
Authenticated: true,
|
||||
Username: "johndoe",
|
||||
Name: "John Doe",
|
||||
@@ -121,7 +120,12 @@ func TestContextController(t *testing.T) {
|
||||
group := router.Group("/api")
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
controller.NewContextController(log, cfg, runtime, group)
|
||||
NewContextController(ContextControllerInput{
|
||||
Log: log,
|
||||
Config: &cfg,
|
||||
Runtime: &runtime,
|
||||
RouterGroup: group,
|
||||
})
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
package controller
|
||||
|
||||
type FrontendLoginFor string
|
||||
|
||||
const (
|
||||
FrontendLoginForOIDC FrontendLoginFor = "oidc"
|
||||
FrontendLoginForApp FrontendLoginFor = "app"
|
||||
)
|
||||
|
||||
type UnauthorizedQuery struct {
|
||||
Username string `url:"username"`
|
||||
Resource string `url:"resource"`
|
||||
@@ -9,4 +16,5 @@ type UnauthorizedQuery struct {
|
||||
|
||||
type RedirectQuery struct {
|
||||
RedirectURI string `url:"redirect_uri"`
|
||||
LoginFor FrontendLoginFor `url:"login_for"`
|
||||
}
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
package controller
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
type HealthController struct {
|
||||
}
|
||||
|
||||
func NewHealthController(router *gin.RouterGroup) *HealthController {
|
||||
type HealthControllerInput struct {
|
||||
dig.In
|
||||
|
||||
RouterGroup *gin.RouterGroup `name:"apiRouterGroup"`
|
||||
}
|
||||
|
||||
func NewHealthController(i HealthControllerInput) *HealthController {
|
||||
controller := &HealthController{}
|
||||
|
||||
router.GET("/healthz", controller.healthHandler)
|
||||
router.HEAD("/healthz", controller.healthHandler)
|
||||
i.RouterGroup.GET("/healthz", controller.healthHandler)
|
||||
i.RouterGroup.HEAD("/healthz", controller.healthHandler)
|
||||
|
||||
return controller
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package controller_test
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||
)
|
||||
|
||||
func TestHealthController(t *testing.T) {
|
||||
@@ -55,7 +54,9 @@ func TestHealthController(t *testing.T) {
|
||||
group := router.Group("/api")
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
controller.NewHealthController(group)
|
||||
NewHealthController(HealthControllerInput{
|
||||
RouterGroup: group,
|
||||
})
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package controller
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
"go.uber.org/dig"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/go-querystring/query"
|
||||
@@ -22,26 +24,30 @@ type OAuthRequest struct {
|
||||
|
||||
type OAuthController struct {
|
||||
log *logger.Logger
|
||||
config model.Config
|
||||
runtime model.RuntimeConfig
|
||||
config *model.Config
|
||||
runtime *model.RuntimeConfig
|
||||
auth *service.AuthService
|
||||
}
|
||||
|
||||
func NewOAuthController(
|
||||
log *logger.Logger,
|
||||
config model.Config,
|
||||
runtimeConfig model.RuntimeConfig,
|
||||
router *gin.RouterGroup,
|
||||
auth *service.AuthService,
|
||||
) *OAuthController {
|
||||
controller := &OAuthController{
|
||||
log: log,
|
||||
config: config,
|
||||
runtime: runtimeConfig,
|
||||
auth: auth,
|
||||
type OAuthControllerInput struct {
|
||||
dig.In
|
||||
|
||||
Log *logger.Logger
|
||||
Config *model.Config
|
||||
RuntimeConfig *model.RuntimeConfig
|
||||
RouterGroup *gin.RouterGroup `name:"apiRouterGroup"`
|
||||
AuthService *service.AuthService
|
||||
}
|
||||
|
||||
oauthGroup := router.Group("/oauth")
|
||||
func NewOAuthController(i OAuthControllerInput) *OAuthController {
|
||||
controller := &OAuthController{
|
||||
log: i.Log,
|
||||
config: i.Config,
|
||||
runtime: i.RuntimeConfig,
|
||||
auth: i.AuthService,
|
||||
}
|
||||
|
||||
oauthGroup := i.RouterGroup.Group("/oauth")
|
||||
oauthGroup.GET("/url/:provider", controller.oauthURLHandler)
|
||||
oauthGroup.GET("/callback/:provider", controller.oauthCallbackHandler)
|
||||
|
||||
@@ -61,7 +67,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var reqParams service.OAuthURLParams
|
||||
var reqParams service.OAuthCallbackParams
|
||||
|
||||
err = c.BindQuery(&reqParams)
|
||||
|
||||
@@ -75,15 +81,13 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
if !controller.isOidcRequest(reqParams) {
|
||||
isRedirectSafe := utils.IsRedirectSafe(reqParams.RedirectURI, controller.runtime.CookieDomain)
|
||||
|
||||
if !isRedirectSafe {
|
||||
if !controller.isRedirectSafe(reqParams.RedirectURI) {
|
||||
controller.log.App.Warn().Str("redirectUri", reqParams.RedirectURI).Msg("Unsafe redirect URI, ignoring")
|
||||
reqParams.RedirectURI = ""
|
||||
}
|
||||
}
|
||||
|
||||
sessionId, _, err := controller.auth.NewOAuthSession(req.Provider, reqParams)
|
||||
sessionId, err := controller.auth.NewOAuthSession(req.Provider, reqParams)
|
||||
|
||||
if err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to create new OAuth session")
|
||||
@@ -183,9 +187,23 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !controller.auth.IsEmailWhitelisted(user.Email) {
|
||||
svc, err := controller.auth.GetOAuthService(sessionIdCookie)
|
||||
|
||||
if err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to get OAuth service for session")
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
if svc.ID() != req.Provider {
|
||||
controller.log.App.Warn().Msgf("OAuth provider mismatch: expected %s, got %s", req.Provider, svc.ID())
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
if !controller.auth.IsEmailWhitelisted(svc.ID(), user.Email) {
|
||||
controller.log.App.Warn().Str("email", user.Email).Msg("Email not whitelisted, denying access")
|
||||
controller.log.AuditLoginFailure(user.Email, req.Provider, c.ClientIP(), "email not whitelisted")
|
||||
controller.log.AuditLoginFailure(user.Email, svc.ID(), c.ClientIP(), "email not whitelisted")
|
||||
|
||||
queries, err := query.Values(UnauthorizedQuery{
|
||||
Username: user.Email,
|
||||
@@ -226,20 +244,6 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
username = strings.Replace(user.Email, "@", "_", 1)
|
||||
}
|
||||
|
||||
svc, err := controller.auth.GetOAuthService(sessionIdCookie)
|
||||
|
||||
if err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to get OAuth service for session")
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
if svc.ID() != req.Provider {
|
||||
controller.log.App.Warn().Msgf("OAuth provider mismatch: expected %s, got %s", req.Provider, svc.ID())
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
sessionCookie := repository.Session{
|
||||
Username: username,
|
||||
Name: name,
|
||||
@@ -272,13 +276,14 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
return
|
||||
}
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/authorize?%s", controller.runtime.AppURL, queries.Encode()))
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/oidc/authorize?%s", controller.runtime.AppURL, queries.Encode()))
|
||||
return
|
||||
}
|
||||
|
||||
if oauthPendingSession.CallbackParams.RedirectURI != "" {
|
||||
queries, err := query.Values(RedirectQuery{
|
||||
RedirectURI: oauthPendingSession.CallbackParams.RedirectURI,
|
||||
LoginFor: FrontendLoginForApp,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -294,16 +299,68 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
c.Redirect(http.StatusTemporaryRedirect, controller.runtime.AppURL)
|
||||
}
|
||||
|
||||
func (controller *OAuthController) isOidcRequest(params service.OAuthURLParams) bool {
|
||||
return params.Scope != "" &&
|
||||
params.ResponseType != "" &&
|
||||
params.ClientID != "" &&
|
||||
params.RedirectURI != ""
|
||||
func (controller *OAuthController) isOidcRequest(params service.OAuthCallbackParams) bool {
|
||||
return params.LoginFor == string(FrontendLoginForOIDC)
|
||||
}
|
||||
|
||||
func (controller *OAuthController) getCookieDomain() string {
|
||||
if controller.config.Auth.SubdomainsEnabled {
|
||||
return "." + controller.runtime.CookieDomain
|
||||
if !controller.config.Auth.SubdomainsEnabled {
|
||||
return ""
|
||||
}
|
||||
return controller.runtime.CookieDomain
|
||||
}
|
||||
|
||||
func (controller *OAuthController) isRedirectSafe(redirectURI string) bool {
|
||||
u, err := url.Parse(redirectURI)
|
||||
|
||||
if err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to parse redirect URI")
|
||||
return false
|
||||
}
|
||||
|
||||
if u.Scheme == "" || u.Host == "" {
|
||||
controller.log.App.Warn().Msg("Redirect URI has invalid scheme or host")
|
||||
return false
|
||||
}
|
||||
|
||||
au, err := url.Parse(controller.runtime.AppURL)
|
||||
|
||||
if err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to parse app URL")
|
||||
return false
|
||||
}
|
||||
|
||||
if u.Scheme != au.Scheme {
|
||||
controller.log.App.Warn().Msg("Redirect URI scheme does not match app URL scheme")
|
||||
return false
|
||||
}
|
||||
|
||||
getEffectivePort := func(u *url.URL) string {
|
||||
if u.Port() != "" {
|
||||
return u.Port()
|
||||
}
|
||||
if u.Scheme == "https" {
|
||||
return "443"
|
||||
}
|
||||
return "80"
|
||||
}
|
||||
|
||||
if getEffectivePort(u) != getEffectivePort(au) {
|
||||
controller.log.App.Warn().Msg("Redirect URI port does not match app URL port")
|
||||
return false
|
||||
}
|
||||
|
||||
if strings.EqualFold(u.Hostname(), au.Hostname()) {
|
||||
return true
|
||||
}
|
||||
|
||||
if !controller.config.Auth.SubdomainsEnabled {
|
||||
return false
|
||||
}
|
||||
|
||||
if strings.HasSuffix(strings.ToLower(u.Hostname()), "."+strings.ToLower(controller.runtime.CookieDomain)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tinyauthapp/tinyauth/internal/test"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
)
|
||||
|
||||
func TestOAuthControllerIsRedirectSafe(t *testing.T) {
|
||||
log := logger.NewLogger().WithTestConfig()
|
||||
log.Init()
|
||||
|
||||
cfg, runtime := test.CreateTestConfigs(t)
|
||||
|
||||
type testCase struct {
|
||||
description string
|
||||
appURL string
|
||||
cookieDomain string
|
||||
subdomainsEnabled bool
|
||||
redirectURI string
|
||||
expected bool
|
||||
}
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
description: "Exact host match returns true",
|
||||
appURL: "https://tinyauth.example.com",
|
||||
cookieDomain: "example.com",
|
||||
subdomainsEnabled: true,
|
||||
redirectURI: "https://tinyauth.example.com",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
description: "Exact host match is case insensitive",
|
||||
appURL: "https://tinyauth.example.com",
|
||||
cookieDomain: "example.com",
|
||||
subdomainsEnabled: true,
|
||||
redirectURI: "https://TinyAuth.Example.com",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
description: "Exact host match with subdomains disabled returns true",
|
||||
appURL: "https://tinyauth.example.com",
|
||||
cookieDomain: "example.com",
|
||||
subdomainsEnabled: false,
|
||||
redirectURI: "https://tinyauth.example.com",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
description: "Subdomain of cookie domain returns true when subdomains enabled",
|
||||
appURL: "https://tinyauth.example.com",
|
||||
cookieDomain: "example.com",
|
||||
subdomainsEnabled: true,
|
||||
redirectURI: "https://sub.example.com",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
description: "Subdomain of cookie domain is case insensitive",
|
||||
appURL: "https://tinyauth.example.com",
|
||||
cookieDomain: "Example.COM",
|
||||
subdomainsEnabled: true,
|
||||
redirectURI: "https://SUB.example.com",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
description: "Subdomain not matching cookie domain returns false",
|
||||
appURL: "https://tinyauth.example.com",
|
||||
cookieDomain: "example.com",
|
||||
subdomainsEnabled: true,
|
||||
redirectURI: "https://sub.evil.com",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
description: "Subdomain returns false when subdomains disabled",
|
||||
appURL: "https://tinyauth.example.com",
|
||||
cookieDomain: "example.com",
|
||||
subdomainsEnabled: false,
|
||||
redirectURI: "https://sub.example.com",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
description: "Cookie domain itself is not a subdomain match",
|
||||
appURL: "https://tinyauth.example.com",
|
||||
cookieDomain: "example.com",
|
||||
subdomainsEnabled: true,
|
||||
redirectURI: "https://example.com",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
description: "Different scheme returns false",
|
||||
appURL: "https://tinyauth.example.com",
|
||||
cookieDomain: "example.com",
|
||||
subdomainsEnabled: true,
|
||||
redirectURI: "http://tinyauth.example.com",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
description: "Different port returns false",
|
||||
appURL: "https://tinyauth.example.com",
|
||||
cookieDomain: "example.com",
|
||||
subdomainsEnabled: true,
|
||||
redirectURI: "https://tinyauth.example.com:8080",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
description: "Empty redirect URI returns false",
|
||||
appURL: "https://tinyauth.example.com",
|
||||
cookieDomain: "example.com",
|
||||
subdomainsEnabled: true,
|
||||
redirectURI: "",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
description: "Redirect URI without host returns false",
|
||||
appURL: "https://tinyauth.example.com",
|
||||
cookieDomain: "example.com",
|
||||
subdomainsEnabled: true,
|
||||
redirectURI: "https:/malicious",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
description: "Redirect URI without scheme returns false",
|
||||
appURL: "https://tinyauth.example.com",
|
||||
cookieDomain: "example.com",
|
||||
subdomainsEnabled: true,
|
||||
redirectURI: "tinyauth.example.com",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
description: "Relative redirect URI returns false",
|
||||
appURL: "https://tinyauth.example.com",
|
||||
cookieDomain: "example.com",
|
||||
subdomainsEnabled: true,
|
||||
redirectURI: "/some/path",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
description: "Userinfo trick with malicious host returns false",
|
||||
appURL: "https://tinyauth.example.com",
|
||||
cookieDomain: "example.com",
|
||||
subdomainsEnabled: true,
|
||||
redirectURI: "https://malicious.example.com@evil.com",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
description: "Unparseable redirect URI returns false",
|
||||
appURL: "https://tinyauth.example.com",
|
||||
cookieDomain: "example.com",
|
||||
subdomainsEnabled: true,
|
||||
redirectURI: "https://exa\x7fmple.com",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
description: "Unparseable app URL returns false",
|
||||
appURL: "https://tinyauth.\x7fexample.com",
|
||||
cookieDomain: "example.com",
|
||||
subdomainsEnabled: true,
|
||||
redirectURI: "https://tinyauth.example.com",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
router := gin.Default()
|
||||
group := router.Group("/api")
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
// Overwrite the app URL, cookie domain and subdomain setting for each test case
|
||||
runtime.AppURL = tc.appURL
|
||||
runtime.CookieDomain = tc.cookieDomain
|
||||
cfg.Auth.SubdomainsEnabled = tc.subdomainsEnabled
|
||||
|
||||
ctrl := NewOAuthController(OAuthControllerInput{
|
||||
Log: log,
|
||||
Config: &cfg,
|
||||
RuntimeConfig: &runtime,
|
||||
RouterGroup: group,
|
||||
})
|
||||
|
||||
assert.Equal(t, tc.expected, ctrl.isRedirectSafe(tc.redirectURI))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,40 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/google/go-querystring/query"
|
||||
"go.uber.org/dig"
|
||||
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
)
|
||||
|
||||
type authorizeErrorParams struct {
|
||||
err error
|
||||
reason string
|
||||
reasonPublic string
|
||||
callback string
|
||||
callbackError string
|
||||
state string
|
||||
json bool
|
||||
}
|
||||
|
||||
type OIDCController struct {
|
||||
log *logger.Logger
|
||||
oidc *service.OIDCService
|
||||
runtime model.RuntimeConfig
|
||||
runtime *model.RuntimeConfig
|
||||
config *model.Config
|
||||
}
|
||||
|
||||
type AuthorizeCallback struct {
|
||||
@@ -56,20 +71,42 @@ type ClientCredentials struct {
|
||||
ClientSecret string
|
||||
}
|
||||
|
||||
func NewOIDCController(
|
||||
log *logger.Logger,
|
||||
oidcService *service.OIDCService,
|
||||
runtimeConfig model.RuntimeConfig,
|
||||
router *gin.RouterGroup) *OIDCController {
|
||||
controller := &OIDCController{
|
||||
log: log,
|
||||
oidc: oidcService,
|
||||
runtime: runtimeConfig,
|
||||
type AuthorizeScreenParams struct {
|
||||
LoginFor FrontendLoginFor `url:"login_for"`
|
||||
OIDCTicket string `url:"oidc_ticket"`
|
||||
OIDCScope string `url:"oidc_scope"`
|
||||
OIDCName string `url:"oidc_name"`
|
||||
OIDCPrompt service.OIDCPrompt `url:"oidc_prompt,omitempty"`
|
||||
}
|
||||
|
||||
oidcGroup := router.Group("/oidc")
|
||||
oidcGroup.GET("/clients/:id", controller.GetClientInfo)
|
||||
oidcGroup.POST("/authorize", controller.Authorize)
|
||||
type AuthorizeCompleteRequest struct {
|
||||
Ticket string `json:"ticket" binding:"required"`
|
||||
}
|
||||
|
||||
type OIDCControllerInput struct {
|
||||
dig.In
|
||||
|
||||
Log *logger.Logger
|
||||
OIDCService *service.OIDCService
|
||||
Config *model.Config
|
||||
RuntimeConfig *model.RuntimeConfig
|
||||
RouterGroup *gin.RouterGroup `name:"apiRouterGroup"`
|
||||
MainRouter *gin.RouterGroup `name:"mainRouterGroup"`
|
||||
}
|
||||
|
||||
func NewOIDCController(i OIDCControllerInput) *OIDCController {
|
||||
controller := &OIDCController{
|
||||
log: i.Log,
|
||||
oidc: i.OIDCService,
|
||||
runtime: i.RuntimeConfig,
|
||||
config: i.Config,
|
||||
}
|
||||
|
||||
i.MainRouter.POST("/authorize", controller.authorize)
|
||||
i.MainRouter.GET("/authorize", controller.authorize)
|
||||
|
||||
oidcGroup := i.RouterGroup.Group("/oidc")
|
||||
oidcGroup.POST("/authorize-complete", controller.authorizeComplete)
|
||||
oidcGroup.POST("/token", controller.Token)
|
||||
oidcGroup.GET("/userinfo", controller.Userinfo)
|
||||
oidcGroup.POST("/userinfo", controller.Userinfo)
|
||||
@@ -77,24 +114,27 @@ func NewOIDCController(
|
||||
return controller
|
||||
}
|
||||
|
||||
func (controller *OIDCController) GetClientInfo(c *gin.Context) {
|
||||
// This endpoint does **not** return a code, it handles param validation, ticket creation
|
||||
// and then redirects to the frontend to handle the consent screen. It performs no destructive
|
||||
// actions (like logging out an existing session)
|
||||
func (controller *OIDCController) authorize(c *gin.Context) {
|
||||
if controller.oidc == nil {
|
||||
controller.log.App.Warn().Msg("Received OIDC client info request but OIDC server is not configured")
|
||||
c.JSON(500, gin.H{
|
||||
"status": 500,
|
||||
"message": "OIDC not configured",
|
||||
controller.authorizeError(c, authorizeErrorParams{
|
||||
err: errors.New("err_oidc_not_configured"),
|
||||
reason: "OIDC not configured",
|
||||
reasonPublic: "This instance is not configured for OIDC",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req ClientRequest
|
||||
req, err := controller.resolveAuthorizeRequest(c)
|
||||
|
||||
err := c.BindUri(&req)
|
||||
if err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to bind URI")
|
||||
c.JSON(400, gin.H{
|
||||
"status": 400,
|
||||
"message": "Bad Request",
|
||||
controller.log.App.Warn().Err(err).Msg("Failed to resolve authorize request")
|
||||
controller.authorizeError(c, authorizeErrorParams{
|
||||
err: err,
|
||||
reason: "Failed to resolve authorize request",
|
||||
reasonPublic: "The authorization request is invalid",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -102,108 +142,273 @@ func (controller *OIDCController) GetClientInfo(c *gin.Context) {
|
||||
client, ok := controller.oidc.GetClient(req.ClientID)
|
||||
|
||||
if !ok {
|
||||
controller.log.App.Warn().Str("clientId", req.ClientID).Msg("Client not found")
|
||||
c.JSON(404, gin.H{
|
||||
"status": 404,
|
||||
"message": "Client not found",
|
||||
controller.authorizeError(c, authorizeErrorParams{
|
||||
err: fmt.Errorf("client not found: %s", req.ClientID),
|
||||
reason: "Client not found",
|
||||
reasonPublic: "The client ID is invalid",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"client": client.ClientID,
|
||||
"name": client.Name,
|
||||
err = controller.oidc.ValidateAuthorizeParams(*req)
|
||||
|
||||
if err != nil {
|
||||
controller.log.App.Warn().Err(err).Msg("Failed to validate authorize params")
|
||||
if err.Error() != "invalid_request_uri" {
|
||||
controller.authorizeError(c, authorizeErrorParams{
|
||||
err: err,
|
||||
reason: "Failed validate authorize params",
|
||||
reasonPublic: "Invalid request parameters",
|
||||
callback: req.RedirectURI,
|
||||
callbackError: err.Error(),
|
||||
state: req.State,
|
||||
})
|
||||
return
|
||||
}
|
||||
controller.authorizeError(c, authorizeErrorParams{
|
||||
err: err,
|
||||
reason: "Redirect URI not trusted",
|
||||
reasonPublic: "The provided redirect URI is not trusted",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (controller *OIDCController) Authorize(c *gin.Context) {
|
||||
if controller.oidc == nil {
|
||||
controller.authorizeError(c, errors.New("err_oidc_not_configured"), "OIDC not configured", "This instance is not configured for OIDC", "", "", "")
|
||||
prompts := controller.oidc.GetPrompt(req.Prompt)
|
||||
|
||||
if slices.Contains(prompts, service.OIDCPromptNone) && len(prompts) > 1 {
|
||||
controller.authorizeError(c, authorizeErrorParams{
|
||||
err: errors.New("invalid prompt"),
|
||||
reason: "Invalid prompt",
|
||||
reasonPublic: "The prompt parameters are invalid",
|
||||
callback: req.RedirectURI,
|
||||
callbackError: "invalid_request",
|
||||
state: req.State,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userContext, err := new(model.UserContext).NewFromGin(c)
|
||||
|
||||
if err != nil {
|
||||
controller.authorizeError(c, err, "Failed to get user context", "User is not logged in or the session is invalid", "", "", "")
|
||||
if !errors.Is(err, model.ErrUserContextNotFound) {
|
||||
controller.log.App.Warn().Err(err).Msg("Failed to get user context")
|
||||
}
|
||||
}
|
||||
|
||||
if (err != nil || !userContext.Authenticated) && slices.Contains(prompts, service.OIDCPromptNone) {
|
||||
controller.authorizeError(c, authorizeErrorParams{
|
||||
err: errors.New("user not logged in"),
|
||||
reason: "User not logged in",
|
||||
reasonPublic: "The user is not logged in",
|
||||
callback: req.RedirectURI,
|
||||
callbackError: "login_required",
|
||||
state: req.State,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !userContext.Authenticated {
|
||||
controller.authorizeError(c, errors.New("err user not logged in"), "User not logged in", "The user is not logged in", "", "", "")
|
||||
ticket := controller.oidc.CreateAuthorizeRequestTicket(*req)
|
||||
|
||||
values := AuthorizeScreenParams{
|
||||
LoginFor: FrontendLoginForOIDC,
|
||||
OIDCTicket: ticket,
|
||||
OIDCScope: req.Scope,
|
||||
OIDCName: client.Name,
|
||||
}
|
||||
|
||||
if slices.Contains(prompts, service.OIDCPromptLogin) {
|
||||
values.OIDCPrompt = service.OIDCPromptLogin
|
||||
} else if slices.Contains(prompts, service.OIDCPromptNone) {
|
||||
values.OIDCPrompt = service.OIDCPromptNone
|
||||
}
|
||||
|
||||
if req.MaxAge != "" && userContext != nil {
|
||||
maxAge, err := strconv.Atoi(req.MaxAge)
|
||||
if err != nil {
|
||||
controller.authorizeError(c, authorizeErrorParams{
|
||||
err: err,
|
||||
reason: "Invalid max_age",
|
||||
reasonPublic: "The max_age parameter is invalid",
|
||||
callback: req.RedirectURI,
|
||||
callbackError: "invalid_request",
|
||||
state: req.State,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req service.AuthorizeRequest
|
||||
if userContext.Authenticated {
|
||||
authTime := time.Unix(userContext.AuthTime, 0)
|
||||
if authTime.Add(time.Duration(maxAge) * time.Second).Before(time.Now()) {
|
||||
values.OIDCPrompt = service.OIDCPromptLogin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cookieId := strings.SplitN(client.ClientID, "-", 2)[0]
|
||||
cookieName := fmt.Sprintf("%s-%s", controller.runtime.ScopeCookieName, cookieId)
|
||||
scopeCookie, err := c.Cookie(cookieName)
|
||||
|
||||
if err == nil {
|
||||
scopes := fmt.Sprintf("scopes=%s;", req.Scope)
|
||||
if controller.oidc.VerifySignedValue(client.ClientSecret, []byte(scopes), scopeCookie) {
|
||||
if values.OIDCPrompt != service.OIDCPromptLogin {
|
||||
values.OIDCPrompt = service.OIDCPromptNone
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
queries, err := query.Values(values)
|
||||
|
||||
if err != nil {
|
||||
controller.authorizeError(c, authorizeErrorParams{
|
||||
err: err,
|
||||
reason: "Failed to compile authorize queries",
|
||||
reasonPublic: "An internal error occured while processing your request",
|
||||
callback: req.RedirectURI,
|
||||
callbackError: "server_error",
|
||||
state: req.State,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
redirectUrl := fmt.Sprintf("%s/oidc/authorize?%s", controller.oidc.GetIssuer(), queries.Encode())
|
||||
c.Redirect(http.StatusFound, redirectUrl)
|
||||
}
|
||||
|
||||
// The actual **internal** endpoint that actually creates the code and session.
|
||||
// It is called by the frontend after the user has logged in and given consent.
|
||||
func (controller *OIDCController) authorizeComplete(c *gin.Context) {
|
||||
if controller.oidc == nil {
|
||||
// For this endpoint we return JSON errors since it's called
|
||||
// by the frontend and not an external client, so there's
|
||||
// no redirect_uri to send the user to in case of error
|
||||
controller.authorizeError(c, authorizeErrorParams{
|
||||
err: errors.New("err_oidc_not_configured"),
|
||||
reason: "OIDC not configured",
|
||||
reasonPublic: "This instance is not configured for OIDC",
|
||||
json: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userContext, err := new(model.UserContext).NewFromGin(c)
|
||||
|
||||
if err != nil {
|
||||
if !errors.Is(err, model.ErrUserContextNotFound) {
|
||||
controller.log.App.Warn().Err(err).Msg("Failed to get user context")
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil || !userContext.Authenticated {
|
||||
controller.authorizeError(c, authorizeErrorParams{
|
||||
err: errors.New("err user not logged in"),
|
||||
reason: "User not logged in",
|
||||
reasonPublic: "The user is not logged in",
|
||||
json: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req AuthorizeCompleteRequest
|
||||
|
||||
err = c.BindJSON(&req)
|
||||
|
||||
if err != nil {
|
||||
controller.authorizeError(c, err, "Failed to bind JSON", "The client provided an invalid authorization request", "", "", "")
|
||||
controller.authorizeError(c, authorizeErrorParams{
|
||||
err: err,
|
||||
reason: "Failed to bind JSON",
|
||||
reasonPublic: "The client provided an invalid authorization request",
|
||||
json: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
client, ok := controller.oidc.GetClient(req.ClientID)
|
||||
authorizeReq, ok := controller.oidc.GetAuthorizeRequestByTicket(req.Ticket)
|
||||
|
||||
if !ok {
|
||||
controller.authorizeError(c, fmt.Errorf("client not found: %s", req.ClientID), "Client not found", "The client ID is invalid", "", "", "")
|
||||
controller.authorizeError(c, authorizeErrorParams{
|
||||
err: errors.New("authorize request not found for ticket"),
|
||||
reason: "Invalid or expired ticket",
|
||||
reasonPublic: "The authorization request has expired or is invalid",
|
||||
json: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err = controller.oidc.ValidateAuthorizeParams(req)
|
||||
// Get the client
|
||||
client, ok := controller.oidc.GetClient(authorizeReq.ClientID)
|
||||
|
||||
if err != nil {
|
||||
controller.log.App.Warn().Err(err).Msg("Failed to validate authorize params")
|
||||
if err.Error() != "invalid_request_uri" {
|
||||
controller.authorizeError(c, err, "Failed validate authorize params", "Invalid request parameters", req.RedirectURI, err.Error(), req.State)
|
||||
return
|
||||
}
|
||||
controller.authorizeError(c, err, "Redirect URI not trusted", "The provided redirect URI is not trusted", "", "", "")
|
||||
if !ok {
|
||||
controller.authorizeError(c, authorizeErrorParams{
|
||||
err: errors.New("client not found"),
|
||||
reason: "Client not found",
|
||||
reasonPublic: "The client is not configured",
|
||||
json: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// WARNING: Since Tinyauth is stateless, we cannot have a sub that never changes. We will just create a uuid out of the username and client name which remains stable, but if username or client name changes then sub changes too.
|
||||
sub := utils.GenerateUUID(fmt.Sprintf("%s:%s", userContext.GetUsername(), client.ID))
|
||||
code := utils.GenerateString(32)
|
||||
// We no longer need the ticket
|
||||
controller.oidc.DeleteAuthorizeRequestTicket(req.Ticket)
|
||||
|
||||
// Create the sub to find and delete old sessions
|
||||
sub := controller.oidc.CreateSub(*userContext, authorizeReq.ClientID)
|
||||
|
||||
// Before storing the code, delete old session
|
||||
err = controller.oidc.DeleteOldSession(c, sub)
|
||||
if err != nil {
|
||||
controller.authorizeError(c, err, "Failed to delete old sessions", "Failed to delete old sessions", req.RedirectURI, "server_error", req.State)
|
||||
controller.authorizeError(c, authorizeErrorParams{
|
||||
err: err,
|
||||
reason: "Failed to delete old sessions",
|
||||
reasonPublic: "Failed to delete old sessions",
|
||||
callback: authorizeReq.RedirectURI,
|
||||
callbackError: "server_error",
|
||||
state: authorizeReq.State,
|
||||
json: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err = controller.oidc.StoreCode(c, sub, code, req)
|
||||
|
||||
if err != nil {
|
||||
controller.authorizeError(c, err, "Failed to store code", "Failed to store code", req.RedirectURI, "server_error", req.State)
|
||||
return
|
||||
}
|
||||
|
||||
// We also need a snapshot of the user that authorized this (skip if no openid scope)
|
||||
if slices.Contains(strings.Fields(req.Scope), "openid") {
|
||||
err = controller.oidc.StoreUserinfo(c, sub, *userContext, req)
|
||||
|
||||
if err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to store user info")
|
||||
controller.authorizeError(c, err, "Failed to store user info", "Failed to store user info", req.RedirectURI, "server_error", req.State)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Create the authorization code
|
||||
code := controller.oidc.CreateCode(*authorizeReq, *userContext)
|
||||
|
||||
queries, err := query.Values(AuthorizeCallback{
|
||||
Code: code,
|
||||
State: req.State,
|
||||
State: authorizeReq.State,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
controller.authorizeError(c, err, "Failed to build query", "Failed to build query", req.RedirectURI, "server_error", req.State)
|
||||
controller.authorizeError(c, authorizeErrorParams{
|
||||
err: err,
|
||||
reason: "Failed to build query",
|
||||
reasonPublic: "Failed to build query",
|
||||
callback: authorizeReq.RedirectURI,
|
||||
callbackError: "server_error",
|
||||
state: authorizeReq.State,
|
||||
json: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Set a cookie for the consent screen (approved scopes)
|
||||
cookieId := strings.SplitN(client.ClientID, "-", 2)[0]
|
||||
cookieName := fmt.Sprintf("%s-%s", controller.runtime.ScopeCookieName, cookieId)
|
||||
scopes := fmt.Sprintf("scopes=%s;", authorizeReq.Scope)
|
||||
|
||||
cookie := &http.Cookie{
|
||||
Name: cookieName,
|
||||
Value: controller.oidc.CreateSignedValue(client.ClientSecret, []byte(scopes)),
|
||||
Path: "/",
|
||||
Secure: controller.config.Auth.SecureCookie,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
|
||||
http.SetCookie(c.Writer, cookie)
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"redirect_uri": fmt.Sprintf("%s?%s", req.RedirectURI, queries.Encode()),
|
||||
"redirect_uri": fmt.Sprintf("%s?%s", authorizeReq.RedirectURI, queries.Encode()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -285,38 +490,33 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
||||
|
||||
switch req.GrantType {
|
||||
case "authorization_code":
|
||||
entry, err := controller.oidc.GetCodeEntry(c, controller.oidc.Hash(req.Code), client.ClientID)
|
||||
entry, ok := controller.oidc.GetCodeEntry(controller.oidc.Hash(req.Code), client.ClientID)
|
||||
|
||||
if !ok {
|
||||
// ensure no code reuse
|
||||
usedCodeSub, ok := controller.oidc.IsCodeUsed(controller.oidc.Hash(req.Code))
|
||||
|
||||
if ok {
|
||||
controller.log.App.Warn().Msg("Code reuse detected")
|
||||
err := controller.oidc.DeleteSessionBySub(c, usedCodeSub)
|
||||
if err != nil {
|
||||
if err := controller.oidc.DeleteTokenByCodeHash(c, controller.oidc.Hash(req.Code)); err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to revoke tokens for replayed code")
|
||||
controller.log.App.Error().Err(err).Msg("Failed to delete session for reused code")
|
||||
}
|
||||
if errors.Is(err, service.ErrCodeNotFound) {
|
||||
c.JSON(400, gin.H{
|
||||
"error": "invalid_grant",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
controller.log.App.Warn().Msg("Code not found")
|
||||
c.JSON(400, gin.H{
|
||||
"error": "invalid_grant",
|
||||
})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, service.ErrCodeExpired) {
|
||||
controller.log.App.Warn().Msg("Code expired")
|
||||
c.JSON(400, gin.H{
|
||||
"error": "invalid_grant",
|
||||
})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, service.ErrInvalidClient) {
|
||||
controller.log.App.Warn().Msg("Code does not belong to client")
|
||||
c.JSON(400, gin.H{
|
||||
"error": "invalid_client",
|
||||
})
|
||||
return
|
||||
}
|
||||
controller.log.App.Error().Err(err).Msg("Failed to get code entry")
|
||||
c.JSON(400, gin.H{
|
||||
"error": "server_error",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// mark code as used to prevent reuse
|
||||
controller.oidc.MarkCodeAsUsed(controller.oidc.Hash(req.Code), entry.Userinfo.Sub)
|
||||
|
||||
if entry.RedirectURI != req.RedirectURI {
|
||||
controller.log.App.Warn().Msg("Redirect URI does not match")
|
||||
@@ -326,7 +526,7 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ok := controller.oidc.ValidatePKCE(entry.CodeChallenge, req.CodeVerifier)
|
||||
ok = controller.oidc.ValidatePKCE(entry.CodeChallenge, req.CodeVerifier)
|
||||
|
||||
if !ok {
|
||||
controller.log.App.Warn().Msg("PKCE validation failed")
|
||||
@@ -336,7 +536,7 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
tokenRes, err := controller.oidc.GenerateAccessToken(c, client, entry)
|
||||
tokenRes, err := controller.oidc.GenerateAccessToken(c, client, *entry, entry.AuthTime)
|
||||
|
||||
if err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to generate access token")
|
||||
@@ -346,7 +546,7 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
tokenResponse = tokenRes
|
||||
tokenResponse = *tokenRes
|
||||
case "refresh_token":
|
||||
tokenRes, err := controller.oidc.RefreshAccessToken(c, req.RefreshToken, creds.ClientID)
|
||||
|
||||
@@ -374,7 +574,7 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
tokenResponse = tokenRes
|
||||
tokenResponse = *tokenRes
|
||||
}
|
||||
|
||||
c.Header("cache-control", "no-store")
|
||||
@@ -438,7 +638,7 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
entry, err := controller.oidc.GetAccessToken(c, controller.oidc.Hash(token))
|
||||
entry, err := controller.oidc.GetSessionByToken(c, controller.oidc.Hash(token))
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrTokenNotFound) {
|
||||
@@ -457,15 +657,17 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
|
||||
}
|
||||
|
||||
// If we don't have the openid scope, return an error
|
||||
if !slices.Contains(strings.Split(entry.Scope, ","), "openid") {
|
||||
controller.log.App.Warn().Msg("OIDC userinfo accessed with token missing openid scope")
|
||||
if !slices.Contains(strings.Split(entry.Scope, " "), "openid") {
|
||||
controller.log.App.Warn().Msg("OIDC userinfo accessed with missing openid scope")
|
||||
c.JSON(401, gin.H{
|
||||
"error": "invalid_scope",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := controller.oidc.GetUserinfo(c, entry.Sub)
|
||||
var userinfo service.UserinfoResponse
|
||||
|
||||
err = json.Unmarshal([]byte(entry.UserinfoJson), &userinfo)
|
||||
|
||||
if err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to get user info")
|
||||
@@ -475,46 +677,55 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, controller.oidc.CompileUserinfo(user, entry.Scope))
|
||||
c.JSON(200, controller.oidc.CompileUserinfo(userinfo, entry.Scope))
|
||||
}
|
||||
|
||||
func (controller *OIDCController) authorizeError(c *gin.Context, err error, reason string, reasonUser string, callback string, callbackError string, state string) {
|
||||
controller.log.App.Warn().Err(err).Str("reason", reason).Msg("Authorization error")
|
||||
func (controller *OIDCController) authorizeError(c *gin.Context, params authorizeErrorParams) {
|
||||
controller.log.App.Error().Err(params.err).Str("reason", params.reason).Msg("Authorization error")
|
||||
|
||||
if callback != "" {
|
||||
if params.callback != "" {
|
||||
errorQueries := CallbackError{
|
||||
Error: callbackError,
|
||||
Error: params.callbackError,
|
||||
}
|
||||
|
||||
if reasonUser != "" {
|
||||
errorQueries.ErrorDescription = reasonUser
|
||||
if params.reasonPublic != "" {
|
||||
errorQueries.ErrorDescription = params.reasonPublic
|
||||
}
|
||||
|
||||
if state != "" {
|
||||
errorQueries.State = state
|
||||
if params.state != "" {
|
||||
errorQueries.State = params.state
|
||||
}
|
||||
|
||||
queries, err := query.Values(errorQueries)
|
||||
|
||||
if err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to build callback error query")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
redirectUrl := fmt.Sprintf("%s?%s", params.callback, queries.Encode())
|
||||
|
||||
if params.json {
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"redirect_uri": fmt.Sprintf("%s?%s", callback, queries.Encode()),
|
||||
"redirect_uri": redirectUrl,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, redirectUrl)
|
||||
return
|
||||
}
|
||||
|
||||
errorQueries := ErrorScreen{
|
||||
Error: reasonUser,
|
||||
Error: params.reasonPublic,
|
||||
}
|
||||
|
||||
queries, err := query.Values(errorQueries)
|
||||
|
||||
if err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to build error query")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -527,8 +738,61 @@ func (controller *OIDCController) authorizeError(c *gin.Context, err error, reas
|
||||
redirectUrl = fmt.Sprintf("%s/error?%s", controller.runtime.AppURL, queries.Encode())
|
||||
}
|
||||
|
||||
if params.json {
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"redirect_uri": redirectUrl,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, redirectUrl)
|
||||
}
|
||||
|
||||
func (controller *OIDCController) resolveAuthorizeRequest(c *gin.Context) (*service.AuthorizeRequest, error) {
|
||||
// step 1: if we have a request object, decode it and ignore other params. If not, bind the params as usual
|
||||
// we check both query and form parameters for the request object since this endpoint can be called with both GET and POST
|
||||
requestObject, err := controller.resolveRequestObject(c)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if requestObject != nil {
|
||||
return requestObject, nil
|
||||
}
|
||||
|
||||
// step 2: by default we assume normal GET query parameters
|
||||
// step 3: if it's a POST request, we try form parameters
|
||||
return controller.resolveNormalParams(c)
|
||||
}
|
||||
|
||||
func (controller *OIDCController) resolveRequestObject(c *gin.Context) (*service.AuthorizeRequest, error) {
|
||||
raw := c.Query("request")
|
||||
|
||||
if raw == "" && c.Request.Method == http.MethodPost {
|
||||
raw = c.PostForm("request")
|
||||
}
|
||||
|
||||
if raw == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return controller.oidc.DecodeAuthorizeJWT(raw)
|
||||
}
|
||||
|
||||
func (controller *OIDCController) resolveNormalParams(c *gin.Context) (*service.AuthorizeRequest, error) {
|
||||
var req service.AuthorizeRequest
|
||||
|
||||
var bind binding.Binding = binding.Query
|
||||
|
||||
if c.Request.Method == http.MethodPost {
|
||||
bind = binding.Form
|
||||
}
|
||||
|
||||
if err := c.ShouldBindWith(&req, bind); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
"go.uber.org/dig"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/go-querystring/query"
|
||||
@@ -53,29 +54,33 @@ type ProxyContext struct {
|
||||
|
||||
type ProxyController struct {
|
||||
log *logger.Logger
|
||||
runtime model.RuntimeConfig
|
||||
runtime *model.RuntimeConfig
|
||||
acls *service.AccessControlsService
|
||||
auth *service.AuthService
|
||||
policyEngine *service.PolicyEngine
|
||||
}
|
||||
|
||||
func NewProxyController(
|
||||
log *logger.Logger,
|
||||
runtime model.RuntimeConfig,
|
||||
router *gin.RouterGroup,
|
||||
acls *service.AccessControlsService,
|
||||
auth *service.AuthService,
|
||||
policyEngine *service.PolicyEngine,
|
||||
) *ProxyController {
|
||||
controller := &ProxyController{
|
||||
log: log,
|
||||
runtime: runtime,
|
||||
acls: acls,
|
||||
auth: auth,
|
||||
policyEngine: policyEngine,
|
||||
type ProxyControllerInput struct {
|
||||
dig.In
|
||||
|
||||
Log *logger.Logger
|
||||
RuntimeConfig *model.RuntimeConfig
|
||||
RouterGroup *gin.RouterGroup `name:"apiRouterGroup"`
|
||||
ACLsService *service.AccessControlsService
|
||||
AuthService *service.AuthService
|
||||
PolicyEngine *service.PolicyEngine
|
||||
}
|
||||
|
||||
proxyGroup := router.Group("/auth")
|
||||
func NewProxyController(i ProxyControllerInput) *ProxyController {
|
||||
controller := &ProxyController{
|
||||
log: i.Log,
|
||||
runtime: i.RuntimeConfig,
|
||||
acls: i.ACLsService,
|
||||
auth: i.AuthService,
|
||||
policyEngine: i.PolicyEngine,
|
||||
}
|
||||
|
||||
proxyGroup := i.RouterGroup.Group("/auth")
|
||||
proxyGroup.Any("/:proxy", controller.proxyHandler)
|
||||
|
||||
return controller
|
||||
@@ -153,14 +158,17 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusTemporaryRedirect, redirectURL)
|
||||
c.Redirect(http.StatusFound, redirectURL)
|
||||
return
|
||||
}
|
||||
|
||||
userContext, err := new(model.UserContext).NewFromGin(c)
|
||||
|
||||
if err != nil {
|
||||
controller.log.App.Debug().Err(err).Msg("Failed to create user context from request, treating as unauthenticated")
|
||||
// No user context found is not an issue
|
||||
if !errors.Is(err, model.ErrUserContextNotFound) {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to create user context from request, treating as unauthenticated")
|
||||
}
|
||||
userContext = &model.UserContext{
|
||||
Authenticated: false,
|
||||
}
|
||||
@@ -199,7 +207,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusTemporaryRedirect, redirectURL)
|
||||
c.Redirect(http.StatusFound, redirectURL)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -243,7 +251,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusTemporaryRedirect, redirectURL)
|
||||
c.Redirect(http.StatusFound, redirectURL)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -272,6 +280,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
|
||||
queries, err := query.Values(RedirectQuery{
|
||||
RedirectURI: fmt.Sprintf("%s://%s%s", proxyCtx.Proto, proxyCtx.Host, proxyCtx.Path),
|
||||
LoginFor: FrontendLoginForApp,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -291,7 +300,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusTemporaryRedirect, redirectURL)
|
||||
c.Redirect(http.StatusFound, redirectURL)
|
||||
}
|
||||
|
||||
func (controller *ProxyController) setHeaders(c *gin.Context, acls *model.App) {
|
||||
@@ -327,7 +336,7 @@ func (controller *ProxyController) handleError(c *gin.Context, proxyCtx ProxyCon
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusTemporaryRedirect, redirectURL)
|
||||
c.Redirect(http.StatusFound, redirectURL)
|
||||
}
|
||||
|
||||
func (controller *ProxyController) getHeader(c *gin.Context, header string) (string, bool) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user