mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2025-10-29 13:15:46 +00:00
Compare commits
93 Commits
v3.2.0-alp
...
v3.3.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5278fbea68 | ||
|
|
773942dc3b | ||
|
|
83483d6374 | ||
|
|
aab01b3195 | ||
|
|
fe5e07139f | ||
|
|
93a75324b8 | ||
|
|
67a01c196f | ||
|
|
483b1de701 | ||
|
|
40ceed6686 | ||
|
|
3878c629c6 | ||
|
|
31e874a34f | ||
|
|
74a346349a | ||
|
|
a9e8bf89a9 | ||
|
|
f824b84787 | ||
|
|
71b0c301f6 | ||
|
|
1c738b718a | ||
|
|
4dc6bc0c98 | ||
|
|
3436466cff | ||
|
|
84f550023a | ||
|
|
9923eb9b8f | ||
|
|
db43f1cb7a | ||
|
|
bedef0bbf2 | ||
|
|
2bfed23db3 | ||
|
|
85ad0d19c7 | ||
|
|
34b1c97db0 | ||
|
|
dc731cff10 | ||
|
|
ab4efdc66c | ||
|
|
4f883260b9 | ||
|
|
0cc85b5d85 | ||
|
|
e11d14fda0 | ||
|
|
7413b3f931 | ||
|
|
b24aab17b1 | ||
|
|
9a7847bc10 | ||
|
|
ef24a760e4 | ||
|
|
8a3fe9fb2c | ||
|
|
04b0a64b18 | ||
|
|
0774012058 | ||
|
|
2253b1bf68 | ||
|
|
10c6f7236d | ||
|
|
45d9d8c1d4 | ||
|
|
ea5b4cefbd | ||
|
|
02faabf688 | ||
|
|
eb36b2211b | ||
|
|
0761c2f5c1 | ||
|
|
476a455329 | ||
|
|
5ec60b8e39 | ||
|
|
5d190fa686 | ||
|
|
1fb9f13c16 | ||
|
|
4a077da11d | ||
|
|
adc8731fb7 | ||
|
|
d9cb738132 | ||
|
|
aeb827265b | ||
|
|
04a0dbb71e | ||
|
|
ed75b6b880 | ||
|
|
f7393da3bb | ||
|
|
529152f70b | ||
|
|
fcc45f6f61 | ||
|
|
8c7856b28c | ||
|
|
77e9fafa32 | ||
|
|
01e2b5e63d | ||
|
|
2d6baef810 | ||
|
|
afad78b7da | ||
|
|
74cd886e9c | ||
|
|
df34b13f25 | ||
|
|
61dfc91110 | ||
|
|
130e6facb7 | ||
|
|
525f4f3041 | ||
|
|
8a21345706 | ||
|
|
1169c633cc | ||
|
|
2242c9c1e6 | ||
|
|
939919df39 | ||
|
|
a579cf37ce | ||
|
|
2647aa07b4 | ||
|
|
f68c580e11 | ||
|
|
9b39a2b856 | ||
|
|
6d17ce699a | ||
|
|
20dbb35d44 | ||
|
|
36d9dd7354 | ||
|
|
5129f9bff8 | ||
|
|
496a56676d | ||
|
|
57e25524c7 | ||
|
|
614a9b468a | ||
|
|
94a5359080 | ||
|
|
38c5cd7b32 | ||
|
|
c664be5cc5 | ||
|
|
bafcb9a867 | ||
|
|
d322c13791 | ||
|
|
8e84e59c2f | ||
|
|
bd7e160e10 | ||
|
|
df849d5a5c | ||
|
|
5cf4e208c6 | ||
|
|
07ddd4f917 | ||
|
|
98abe514e1 |
@@ -12,9 +12,6 @@ GITHUB_CLIENT_SECRET_FILE=github_client_secret_file
|
|||||||
GOOGLE_CLIENT_ID=google_client_id
|
GOOGLE_CLIENT_ID=google_client_id
|
||||||
GOOGLE_CLIENT_SECRET=google_client_secret
|
GOOGLE_CLIENT_SECRET=google_client_secret
|
||||||
GOOGLE_CLIENT_SECRET_FILE=google_client_secret_file
|
GOOGLE_CLIENT_SECRET_FILE=google_client_secret_file
|
||||||
TAILSCALE_CLIENT_ID=tailscale_client_id
|
|
||||||
TAILSCALE_CLIENT_SECRET=tailscale_client_secret
|
|
||||||
TAILSCALE_CLIENT_SECRET_FILE=tailscale__client_secret_file
|
|
||||||
GENERIC_CLIENT_ID=generic_client_id
|
GENERIC_CLIENT_ID=generic_client_id
|
||||||
GENERIC_CLIENT_SECRET=generic_client_secret
|
GENERIC_CLIENT_SECRET=generic_client_secret
|
||||||
GENERIC_CLIENT_SECRET_FILE=generic_client_secret_file
|
GENERIC_CLIENT_SECRET_FILE=generic_client_secret_file
|
||||||
@@ -26,5 +23,9 @@ DISABLE_CONTINUE=false
|
|||||||
OAUTH_WHITELIST=
|
OAUTH_WHITELIST=
|
||||||
GENERIC_NAME=My OAuth
|
GENERIC_NAME=My OAuth
|
||||||
SESSION_EXPIRY=7200
|
SESSION_EXPIRY=7200
|
||||||
|
LOGIN_TIMEOUT=300
|
||||||
|
LOGIN_MAX_RETRIES=5
|
||||||
LOG_LEVEL=0
|
LOG_LEVEL=0
|
||||||
APP_TITLE=Tinyauth SSO
|
APP_TITLE=Tinyauth SSO
|
||||||
|
FORGOT_PASSWORD_MESSAGE=Some message about resetting the password
|
||||||
|
OAUTH_AUTO_REDIRECT=none
|
||||||
14
.github/dependabot.yml
vendored
Normal file
14
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "bun"
|
||||||
|
directory: "/frontend"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
- package-ecosystem: "gomod"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
- package-ecosystem: "docker"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -4,11 +4,9 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
ci:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|||||||
105
.github/workflows/release.yml
vendored
105
.github/workflows/release.yml
vendored
@@ -6,7 +6,85 @@ on:
|
|||||||
- "v*"
|
- "v*"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
binary-build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: "^1.23.2"
|
||||||
|
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
run: |
|
||||||
|
cd frontend
|
||||||
|
bun install
|
||||||
|
|
||||||
|
- name: Install backend dependencies
|
||||||
|
run: |
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
run: |
|
||||||
|
cd frontend
|
||||||
|
bun run build
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
cp -r frontend/dist internal/assets/dist
|
||||||
|
CGO_ENABLED=0 go build -ldflags "-s -w" -o tinyauth-amd64
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: tinyauth-amd64
|
||||||
|
path: tinyauth-amd64
|
||||||
|
|
||||||
|
binary-build-arm:
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: "^1.23.2"
|
||||||
|
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
run: |
|
||||||
|
cd frontend
|
||||||
|
bun install
|
||||||
|
|
||||||
|
- name: Install backend dependencies
|
||||||
|
run: |
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
run: |
|
||||||
|
cd frontend
|
||||||
|
bun run build
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
cp -r frontend/dist internal/assets/dist
|
||||||
|
CGO_ENABLED=0 go build -ldflags "-s -w" -o tinyauth-arm64
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: tinyauth-arm64
|
||||||
|
path: tinyauth-arm64
|
||||||
|
|
||||||
|
image-build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -51,7 +129,7 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
build-arm:
|
image-build-arm:
|
||||||
runs-on: ubuntu-24.04-arm
|
runs-on: ubuntu-24.04-arm
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -96,11 +174,11 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
merge:
|
image-merge:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- build
|
- image-build
|
||||||
- build-arm
|
- image-build-arm
|
||||||
steps:
|
steps:
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
@@ -134,3 +212,20 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||||
$(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *)
|
$(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *)
|
||||||
|
|
||||||
|
update-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- binary-build
|
||||||
|
- binary-build-arm
|
||||||
|
steps:
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: tinyauth-*
|
||||||
|
path: binaries
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: binaries/*
|
||||||
|
|||||||
30
.github/workflows/sponsors.yml
vendored
Normal file
30
.github/workflows/sponsors.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: Generate Sponsors List
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
generate-sponsors:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Generate Sponsors
|
||||||
|
uses: JamesIves/github-sponsors-readme-action@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.SPONSORS_GENERATOR_PAT }}
|
||||||
|
file: README.md
|
||||||
|
template: '<a href="https://github.com/{{{ login }}}"><img src="{{{ avatarUrl }}}" width="64px" alt="User avatar: {{{ login }}}" /></a> '
|
||||||
|
|
||||||
|
- name: Create Pull Request
|
||||||
|
uses: peter-evans/create-pull-request@v7
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
commit-message: |
|
||||||
|
docs: regenerate readme sponsors list
|
||||||
|
committer: GitHub <noreply@github.com>
|
||||||
|
author: GitHub <noreply@github.com>
|
||||||
|
branch: docs/update-readme
|
||||||
|
title: |
|
||||||
|
docs: regenerate readme sponsors list
|
||||||
|
labels: bot
|
||||||
20
.github/workflows/stale.yml
vendored
Normal file
20
.github/workflows/stale.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
name: Close stale issues and PRs
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: 0 10 * * *
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v9
|
||||||
|
with:
|
||||||
|
days-before-stale: 30
|
||||||
|
stale-pr-message: This PR has been inactive for 30 days and will be marked as stale.
|
||||||
|
stale-issue-message: This issue has been inactive for 30 days and will be marked as stale.
|
||||||
|
close-issue-message: Closed for inactivity.
|
||||||
|
close-pr-message: Closed for inactivity.
|
||||||
|
stale-issue-label: stale
|
||||||
|
stale-pr-label: stale
|
||||||
|
exempt-issue-labels: pinned
|
||||||
|
exempt-pr-labels: pinned
|
||||||
58
.github/workflows/translations.yml
vendored
58
.github/workflows/translations.yml
vendored
@@ -3,7 +3,7 @@ name: Publish translations
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- i18n_v*
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -16,7 +16,53 @@ concurrency:
|
|||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
get-branches:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
i18n-branches: ${{ steps.get-branches.outputs.result }}
|
||||||
|
steps:
|
||||||
|
- name: Get branches
|
||||||
|
id: get-branches
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const { data: repos } = await github.rest.repos.listBranches({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
})
|
||||||
|
|
||||||
|
const i18nBranches = repos.filter((branch) => branch.name.startsWith("i18n_v"))
|
||||||
|
const i18nBranchNames = i18nBranches.map((branch) => branch.name)
|
||||||
|
|
||||||
|
return i18nBranchNames
|
||||||
|
|
||||||
|
get-translations:
|
||||||
|
needs: get-branches
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
branch: ${{ fromJson(needs.get-branches.outputs.i18n-branches) }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ matrix.branch }}
|
||||||
|
|
||||||
|
- name: Get translation version
|
||||||
|
id: get-version
|
||||||
|
run: |
|
||||||
|
branch=${{ matrix.branch }}
|
||||||
|
version=${branch#i18n_}
|
||||||
|
echo "version=$version" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Upload translations
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ steps.get-version.outputs.version }}
|
||||||
|
path: frontend/src/lib/i18n/locales
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
needs: get-translations
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -25,10 +71,14 @@ jobs:
|
|||||||
- name: Setup Pages
|
- name: Setup Pages
|
||||||
uses: actions/configure-pages@v4
|
uses: actions/configure-pages@v4
|
||||||
|
|
||||||
- name: Move translations
|
- name: Prepare output directory
|
||||||
run: |
|
run: |
|
||||||
mkdir -p dist
|
mkdir -p dist/i18n/
|
||||||
mv frontend/src/lib/i18n/locales dist/i18n
|
|
||||||
|
- name: Download translations
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: dist/i18n/
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v3
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Site builder
|
# Site builder
|
||||||
FROM oven/bun:1.1.45-alpine AS frontend-builder
|
FROM oven/bun:1.2.11-alpine AS frontend-builder
|
||||||
|
|
||||||
WORKDIR /frontend
|
WORKDIR /frontend
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ COPY ./frontend/postcss.config.cjs ./
|
|||||||
RUN bun run build
|
RUN bun run build
|
||||||
|
|
||||||
# Builder
|
# Builder
|
||||||
FROM golang:1.23-alpine3.21 AS builder
|
FROM golang:1.24-alpine3.21 AS builder
|
||||||
|
|
||||||
WORKDIR /tinyauth
|
WORKDIR /tinyauth
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.23-alpine3.21
|
FROM golang:1.24-alpine3.21
|
||||||
|
|
||||||
WORKDIR /tinyauth
|
WORKDIR /tinyauth
|
||||||
|
|
||||||
@@ -12,9 +12,6 @@ COPY ./internal ./internal
|
|||||||
COPY ./main.go ./
|
COPY ./main.go ./
|
||||||
COPY ./air.toml ./
|
COPY ./air.toml ./
|
||||||
|
|
||||||
RUN mkdir -p ./internal/assets/dist && \
|
|
||||||
echo "app running" > ./internal/assets/dist/index.html
|
|
||||||
|
|
||||||
RUN go install github.com/air-verse/air@v1.61.7
|
RUN go install github.com/air-verse/air@v1.61.7
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -1,5 +1,5 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img alt="Tinyauth" title="Tinyauth" width="256" src="frontend/public/logo.png">
|
<img alt="Tinyauth" title="Tinyauth" height="256" src="frontend/public/logo.png">
|
||||||
<h1>Tinyauth</h1>
|
<h1>Tinyauth</h1>
|
||||||
<p>The easiest way to secure your apps with a login screen.</p>
|
<p>The easiest way to secure your apps with a login screen.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -15,7 +15,9 @@
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic OAuth provider to all of your docker apps. It is made for traefik but it can be extended to work with all reverse proxies like caddy and nginx.
|
Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic provider to all of your docker apps. It is designed for traefik but it can be extended to work with all reverse proxies like caddy and nginx.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> Tinyauth is in active development and configuration may change often. Please make sure to carefully read the release notes before updating.
|
> Tinyauth is in active development and configuration may change often. Please make sure to carefully read the release notes before updating.
|
||||||
@@ -29,11 +31,11 @@ I just made a Discord server for Tinyauth! It is not only for Tinyauth but gener
|
|||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
You can easily get started with tinyauth by following the guide on the [documentation](https://tinyauth.app/docs/getting-started.html). There is also an available [docker compose file](./docker-compose.example.yml) that has traefik, nginx and tinyauth to demonstrate its capabilities.
|
You can easily get started with tinyauth by following the guide in the [documentation](https://tinyauth.app/docs/getting-started.html). There is also an available [docker compose file](./docker-compose.example.yml) that has traefik, whoami and tinyauth to demonstrate its capabilities.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
You can find documentation and guides on all available configuration of tinyauth [here](https://tinyauth.app).
|
You can find documentation and guides on all of the available configuration of tinyauth [here](https://tinyauth.app).
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
@@ -51,9 +53,7 @@ Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You ma
|
|||||||
|
|
||||||
Thanks a lot to the following people for providing me with more coffee:
|
Thanks a lot to the following people for providing me with more coffee:
|
||||||
|
|
||||||
| <img height="64" src="https://avatars.githubusercontent.com/u/47644445?v=4" alt="Nicolas"> | <img height="64" src="https://avatars.githubusercontent.com/u/4255748?v=4" alt="Erwin"> |
|
<!-- sponsors --><a href="https://github.com/nicotsx"><img src="https://github.com/nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a> <a href="https://github.com/SimpleHomelab"><img src="https://github.com/SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a> <a href="https://github.com/jmadden91"><img src="https://github.com/jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a> <a href="https://github.com/tribor"><img src="https://github.com/tribor.png" width="64px" alt="User avatar: tribor" /></a> <!-- sponsors -->
|
||||||
| ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
|
|
||||||
| <div align="center"><a href="https://github.com/nicotsx">Nicolas</a></div> | <div align="center"><a href="https://github.com/erwinkramer">Erwin</a></div> |
|
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|
||||||
@@ -61,3 +61,7 @@ Credits for the logo of this app go to:
|
|||||||
|
|
||||||
- **Freepik** for providing the police hat and badge.
|
- **Freepik** for providing the police hat and badge.
|
||||||
- **Renee French** for the original gopher logo.
|
- **Renee French** for the original gopher logo.
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
[](https://www.star-history.com/#steveiliop56/tinyauth&Date)
|
||||||
|
|||||||
1
air.toml
1
air.toml
@@ -2,6 +2,7 @@ root = "/tinyauth"
|
|||||||
tmp_dir = "tmp"
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
|
pre_cmd = ["mkdir -p internal/assets/dist", "echo 'backend running' > internal/assets/dist/index.html"]
|
||||||
cmd = "go build -o ./tmp/tinyauth ."
|
cmd = "go build -o ./tmp/tinyauth ."
|
||||||
bin = "tmp/tinyauth"
|
bin = "tmp/tinyauth"
|
||||||
include_ext = ["go"]
|
include_ext = ["go"]
|
||||||
|
|||||||
BIN
assets/screenshot.png
Normal file
BIN
assets/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
94
cmd/root.go
94
cmd/root.go
@@ -2,7 +2,6 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -43,7 +42,6 @@ var rootCmd = &cobra.Command{
|
|||||||
config.GithubClientSecret = utils.GetSecret(config.GithubClientSecret, config.GithubClientSecretFile)
|
config.GithubClientSecret = utils.GetSecret(config.GithubClientSecret, config.GithubClientSecretFile)
|
||||||
config.GoogleClientSecret = utils.GetSecret(config.GoogleClientSecret, config.GoogleClientSecretFile)
|
config.GoogleClientSecret = utils.GetSecret(config.GoogleClientSecret, config.GoogleClientSecretFile)
|
||||||
config.GenericClientSecret = utils.GetSecret(config.GenericClientSecret, config.GenericClientSecretFile)
|
config.GenericClientSecret = utils.GetSecret(config.GenericClientSecret, config.GenericClientSecretFile)
|
||||||
config.TailscaleClientSecret = utils.GetSecret(config.TailscaleClientSecret, config.TailscaleClientSecretFile)
|
|
||||||
|
|
||||||
// Validate config
|
// Validate config
|
||||||
validator := validator.New()
|
validator := validator.New()
|
||||||
@@ -63,13 +61,6 @@ var rootCmd = &cobra.Command{
|
|||||||
HandleError(errors.New("no users or OAuth configured"), "No users or OAuth configured")
|
HandleError(errors.New("no users or OAuth configured"), "No users or OAuth configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create oauth whitelist
|
|
||||||
oauthWhitelist := utils.Filter(strings.Split(config.OAuthWhitelist, ","), func(val string) bool {
|
|
||||||
return val != ""
|
|
||||||
})
|
|
||||||
|
|
||||||
log.Debug().Msg("Parsed OAuth whitelist")
|
|
||||||
|
|
||||||
// Get domain
|
// Get domain
|
||||||
log.Debug().Msg("Getting domain")
|
log.Debug().Msg("Getting domain")
|
||||||
domain, err := utils.GetUpperDomain(config.AppURL)
|
domain, err := utils.GetUpperDomain(config.AppURL)
|
||||||
@@ -78,39 +69,52 @@ var rootCmd = &cobra.Command{
|
|||||||
|
|
||||||
// Create OAuth config
|
// Create OAuth config
|
||||||
oauthConfig := types.OAuthConfig{
|
oauthConfig := types.OAuthConfig{
|
||||||
GithubClientId: config.GithubClientId,
|
GithubClientId: config.GithubClientId,
|
||||||
GithubClientSecret: config.GithubClientSecret,
|
GithubClientSecret: config.GithubClientSecret,
|
||||||
GoogleClientId: config.GoogleClientId,
|
GoogleClientId: config.GoogleClientId,
|
||||||
GoogleClientSecret: config.GoogleClientSecret,
|
GoogleClientSecret: config.GoogleClientSecret,
|
||||||
TailscaleClientId: config.TailscaleClientId,
|
GenericClientId: config.GenericClientId,
|
||||||
TailscaleClientSecret: config.TailscaleClientSecret,
|
GenericClientSecret: config.GenericClientSecret,
|
||||||
GenericClientId: config.GenericClientId,
|
GenericScopes: strings.Split(config.GenericScopes, ","),
|
||||||
GenericClientSecret: config.GenericClientSecret,
|
GenericAuthURL: config.GenericAuthURL,
|
||||||
GenericScopes: strings.Split(config.GenericScopes, ","),
|
GenericTokenURL: config.GenericTokenURL,
|
||||||
GenericAuthURL: config.GenericAuthURL,
|
GenericUserURL: config.GenericUserURL,
|
||||||
GenericTokenURL: config.GenericTokenURL,
|
AppURL: config.AppURL,
|
||||||
GenericUserURL: config.GenericUserURL,
|
|
||||||
AppURL: config.AppURL,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create handlers config
|
// Create handlers config
|
||||||
serverConfig := types.HandlersConfig{
|
handlersConfig := types.HandlersConfig{
|
||||||
AppURL: config.AppURL,
|
AppURL: config.AppURL,
|
||||||
Domain: fmt.Sprintf(".%s", domain),
|
DisableContinue: config.DisableContinue,
|
||||||
CookieSecure: config.CookieSecure,
|
Title: config.Title,
|
||||||
DisableContinue: config.DisableContinue,
|
GenericName: config.GenericName,
|
||||||
Title: config.Title,
|
CookieSecure: config.CookieSecure,
|
||||||
GenericName: config.GenericName,
|
Domain: domain,
|
||||||
|
ForgotPasswordMessage: config.FogotPasswordMessage,
|
||||||
|
OAuthAutoRedirect: config.OAuthAutoRedirect,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create api config
|
// Create api config
|
||||||
apiConfig := types.APIConfig{
|
apiConfig := types.APIConfig{
|
||||||
Port: config.Port,
|
Port: config.Port,
|
||||||
Address: config.Address,
|
Address: config.Address,
|
||||||
Secret: config.Secret,
|
}
|
||||||
CookieSecure: config.CookieSecure,
|
|
||||||
SessionExpiry: config.SessionExpiry,
|
// Create auth config
|
||||||
Domain: domain,
|
authConfig := types.AuthConfig{
|
||||||
|
Users: users,
|
||||||
|
OauthWhitelist: config.OAuthWhitelist,
|
||||||
|
Secret: config.Secret,
|
||||||
|
CookieSecure: config.CookieSecure,
|
||||||
|
SessionExpiry: config.SessionExpiry,
|
||||||
|
Domain: domain,
|
||||||
|
LoginTimeout: config.LoginTimeout,
|
||||||
|
LoginMaxRetries: config.LoginMaxRetries,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create hooks config
|
||||||
|
hooksConfig := types.HooksConfig{
|
||||||
|
Domain: domain,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create docker service
|
// Create docker service
|
||||||
@@ -121,7 +125,7 @@ var rootCmd = &cobra.Command{
|
|||||||
HandleError(err, "Failed to initialize docker")
|
HandleError(err, "Failed to initialize docker")
|
||||||
|
|
||||||
// Create auth service
|
// Create auth service
|
||||||
auth := auth.NewAuth(docker, users, oauthWhitelist, config.SessionExpiry)
|
auth := auth.NewAuth(authConfig, docker)
|
||||||
|
|
||||||
// Create OAuth providers service
|
// Create OAuth providers service
|
||||||
providers := providers.NewProviders(oauthConfig)
|
providers := providers.NewProviders(oauthConfig)
|
||||||
@@ -130,10 +134,10 @@ var rootCmd = &cobra.Command{
|
|||||||
providers.Init()
|
providers.Init()
|
||||||
|
|
||||||
// Create hooks service
|
// Create hooks service
|
||||||
hooks := hooks.NewHooks(auth, providers)
|
hooks := hooks.NewHooks(hooksConfig, auth, providers)
|
||||||
|
|
||||||
// Create handlers
|
// Create handlers
|
||||||
handlers := handlers.NewHandlers(serverConfig, auth, hooks, providers, docker)
|
handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker)
|
||||||
|
|
||||||
// Create API
|
// Create API
|
||||||
api := api.NewAPI(apiConfig, handlers)
|
api := api.NewAPI(apiConfig, handlers)
|
||||||
@@ -184,9 +188,6 @@ func init() {
|
|||||||
rootCmd.Flags().String("google-client-id", "", "Google OAuth client ID.")
|
rootCmd.Flags().String("google-client-id", "", "Google OAuth client ID.")
|
||||||
rootCmd.Flags().String("google-client-secret", "", "Google OAuth client secret.")
|
rootCmd.Flags().String("google-client-secret", "", "Google OAuth client secret.")
|
||||||
rootCmd.Flags().String("google-client-secret-file", "", "Google OAuth client secret file.")
|
rootCmd.Flags().String("google-client-secret-file", "", "Google OAuth client secret file.")
|
||||||
rootCmd.Flags().String("tailscale-client-id", "", "Tailscale OAuth client ID.")
|
|
||||||
rootCmd.Flags().String("tailscale-client-secret", "", "Tailscale OAuth client secret.")
|
|
||||||
rootCmd.Flags().String("tailscale-client-secret-file", "", "Tailscale OAuth client secret file.")
|
|
||||||
rootCmd.Flags().String("generic-client-id", "", "Generic OAuth client ID.")
|
rootCmd.Flags().String("generic-client-id", "", "Generic OAuth client ID.")
|
||||||
rootCmd.Flags().String("generic-client-secret", "", "Generic OAuth client secret.")
|
rootCmd.Flags().String("generic-client-secret", "", "Generic OAuth client secret.")
|
||||||
rootCmd.Flags().String("generic-client-secret-file", "", "Generic OAuth client secret file.")
|
rootCmd.Flags().String("generic-client-secret-file", "", "Generic OAuth client secret file.")
|
||||||
@@ -197,9 +198,13 @@ func init() {
|
|||||||
rootCmd.Flags().String("generic-name", "Generic", "Generic OAuth provider name.")
|
rootCmd.Flags().String("generic-name", "Generic", "Generic OAuth provider name.")
|
||||||
rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.")
|
rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.")
|
||||||
rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.")
|
rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.")
|
||||||
|
rootCmd.Flags().String("oauth-auto-redirect", "none", "Auto redirect to the specified OAuth provider if configured. (available providers: github, google, generic)")
|
||||||
rootCmd.Flags().Int("session-expiry", 86400, "Session (cookie) expiration time in seconds.")
|
rootCmd.Flags().Int("session-expiry", 86400, "Session (cookie) expiration time in seconds.")
|
||||||
|
rootCmd.Flags().Int("login-timeout", 300, "Login timeout in seconds after max retries reached (0 to disable).")
|
||||||
|
rootCmd.Flags().Int("login-max-retries", 5, "Maximum login attempts before timeout (0 to disable).")
|
||||||
rootCmd.Flags().Int("log-level", 1, "Log level.")
|
rootCmd.Flags().Int("log-level", 1, "Log level.")
|
||||||
rootCmd.Flags().String("app-title", "Tinyauth", "Title of the app.")
|
rootCmd.Flags().String("app-title", "Tinyauth", "Title of the app.")
|
||||||
|
rootCmd.Flags().String("forgot-password-message", "You can reset your password by changing the `USERS` environment variable.", "Message to show on the forgot password page.")
|
||||||
|
|
||||||
// Bind flags to environment
|
// Bind flags to environment
|
||||||
viper.BindEnv("port", "PORT")
|
viper.BindEnv("port", "PORT")
|
||||||
@@ -216,9 +221,6 @@ func init() {
|
|||||||
viper.BindEnv("google-client-id", "GOOGLE_CLIENT_ID")
|
viper.BindEnv("google-client-id", "GOOGLE_CLIENT_ID")
|
||||||
viper.BindEnv("google-client-secret", "GOOGLE_CLIENT_SECRET")
|
viper.BindEnv("google-client-secret", "GOOGLE_CLIENT_SECRET")
|
||||||
viper.BindEnv("google-client-secret-file", "GOOGLE_CLIENT_SECRET_FILE")
|
viper.BindEnv("google-client-secret-file", "GOOGLE_CLIENT_SECRET_FILE")
|
||||||
viper.BindEnv("tailscale-client-id", "TAILSCALE_CLIENT_ID")
|
|
||||||
viper.BindEnv("tailscale-client-secret", "TAILSCALE_CLIENT_SECRET")
|
|
||||||
viper.BindEnv("tailscale-client-secret-file", "TAILSCALE_CLIENT_SECRET_FILE")
|
|
||||||
viper.BindEnv("generic-client-id", "GENERIC_CLIENT_ID")
|
viper.BindEnv("generic-client-id", "GENERIC_CLIENT_ID")
|
||||||
viper.BindEnv("generic-client-secret", "GENERIC_CLIENT_SECRET")
|
viper.BindEnv("generic-client-secret", "GENERIC_CLIENT_SECRET")
|
||||||
viper.BindEnv("generic-client-secret-file", "GENERIC_CLIENT_SECRET_FILE")
|
viper.BindEnv("generic-client-secret-file", "GENERIC_CLIENT_SECRET_FILE")
|
||||||
@@ -229,9 +231,13 @@ func init() {
|
|||||||
viper.BindEnv("generic-name", "GENERIC_NAME")
|
viper.BindEnv("generic-name", "GENERIC_NAME")
|
||||||
viper.BindEnv("disable-continue", "DISABLE_CONTINUE")
|
viper.BindEnv("disable-continue", "DISABLE_CONTINUE")
|
||||||
viper.BindEnv("oauth-whitelist", "OAUTH_WHITELIST")
|
viper.BindEnv("oauth-whitelist", "OAUTH_WHITELIST")
|
||||||
|
viper.BindEnv("oauth-auto-redirect", "OAUTH_AUTO_REDIRECT")
|
||||||
viper.BindEnv("session-expiry", "SESSION_EXPIRY")
|
viper.BindEnv("session-expiry", "SESSION_EXPIRY")
|
||||||
viper.BindEnv("log-level", "LOG_LEVEL")
|
viper.BindEnv("log-level", "LOG_LEVEL")
|
||||||
viper.BindEnv("app-title", "APP_TITLE")
|
viper.BindEnv("app-title", "APP_TITLE")
|
||||||
|
viper.BindEnv("login-timeout", "LOGIN_TIMEOUT")
|
||||||
|
viper.BindEnv("login-max-retries", "LOGIN_MAX_RETRIES")
|
||||||
|
viper.BindEnv("forgot-password-message", "FORGOT_PASSWORD_MESSAGE")
|
||||||
|
|
||||||
// Bind flags to viper
|
// Bind flags to viper
|
||||||
viper.BindPFlags(rootCmd.Flags())
|
viper.BindPFlags(rootCmd.Flags())
|
||||||
|
|||||||
Binary file not shown.
3162
frontend/package-lock.json
generated
3162
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,34 +14,35 @@
|
|||||||
"@mantine/form": "^7.16.0",
|
"@mantine/form": "^7.16.0",
|
||||||
"@mantine/hooks": "^7.16.0",
|
"@mantine/hooks": "^7.16.0",
|
||||||
"@mantine/notifications": "^7.16.0",
|
"@mantine/notifications": "^7.16.0",
|
||||||
"@tanstack/react-query": "4",
|
"@tanstack/react-query": "5",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"i18next": "^24.2.3",
|
"i18next": "^25.0.0",
|
||||||
"i18next-browser-languagedetector": "^8.0.4",
|
"i18next-browser-languagedetector": "^8.0.4",
|
||||||
"i18next-chained-backend": "^4.6.2",
|
"i18next-chained-backend": "^4.6.2",
|
||||||
"i18next-http-backend": "^3.0.2",
|
"i18next-http-backend": "^3.0.2",
|
||||||
"i18next-resources-to-backend": "^1.2.1",
|
"i18next-resources-to-backend": "^1.2.1",
|
||||||
"react": "^18.3.1",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^19.1.0",
|
||||||
"react-i18next": "^15.4.1",
|
"react-i18next": "^15.4.1",
|
||||||
"react-router": "^7.1.3",
|
"react-markdown": "^10.1.0",
|
||||||
|
"react-router": "^7.5.2",
|
||||||
"zod": "^3.24.1"
|
"zod": "^3.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
"@types/react": "^18.3.18",
|
"@types/react": "^19.1.1",
|
||||||
"@types/react-dom": "^18.3.5",
|
"@types/react-dom": "^19.1.2",
|
||||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||||
"eslint": "^9.17.0",
|
"eslint": "^9.17.0",
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.16",
|
"eslint-plugin-react-refresh": "^0.4.16",
|
||||||
"globals": "^15.14.0",
|
"globals": "^16.0.0",
|
||||||
"postcss": "^8.5.1",
|
"postcss": "^8.5.1",
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"prettier": "3.4.2",
|
"prettier": "3.5.3",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.8.3",
|
||||||
"typescript-eslint": "^8.18.2",
|
"typescript-eslint": "^8.18.2",
|
||||||
"vite": "^6.0.5"
|
"vite": "^6.3.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import { TextInput, PasswordInput, Button } from "@mantine/core";
|
import { TextInput, PasswordInput, Button, Anchor, Group, Text } from "@mantine/core";
|
||||||
import { useForm, zodResolver } from "@mantine/form";
|
import { useForm, zodResolver } from "@mantine/form";
|
||||||
import { LoginFormValues, loginSchema } from "../../schemas/login-schema";
|
import { LoginFormValues, loginSchema } from "../../schemas/login-schema";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface LoginFormProps {
|
interface LoginFormProps {
|
||||||
isLoading: boolean;
|
isPending: boolean;
|
||||||
onSubmit: (values: LoginFormValues) => void;
|
onSubmit: (values: LoginFormValues) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LoginForm = (props: LoginFormProps) => {
|
export const LoginForm = (props: LoginFormProps) => {
|
||||||
const { isLoading, onSubmit } = props;
|
const { isPending, onSubmit } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
@@ -25,22 +25,31 @@ export const LoginForm = (props: LoginFormProps) => {
|
|||||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t("loginUsername")}
|
label={t("loginUsername")}
|
||||||
placeholder="user@example.com"
|
placeholder="Username"
|
||||||
|
disabled={isPending}
|
||||||
required
|
required
|
||||||
disabled={isLoading}
|
withAsterisk={false}
|
||||||
key={form.key("username")}
|
key={form.key("username")}
|
||||||
{...form.getInputProps("username")}
|
{...form.getInputProps("username")}
|
||||||
/>
|
/>
|
||||||
|
<Group justify="space-between" mb={5} mt="md">
|
||||||
|
<Text component="label" htmlFor=".password-input" size="sm" fw={500}>
|
||||||
|
{t("loginPassword")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Anchor href="#" onClick={() => window.location.replace("/forgot-password")} pt={2} fw={500} fz="xs">
|
||||||
|
{t('forgotPasswordTitle')}
|
||||||
|
</Anchor>
|
||||||
|
</Group>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label={t("loginPassword")}
|
className="password-input"
|
||||||
placeholder="password"
|
placeholder="Password"
|
||||||
required
|
required
|
||||||
mt="md"
|
disabled={isPending}
|
||||||
disabled={isLoading}
|
|
||||||
key={form.key("password")}
|
key={form.key("password")}
|
||||||
{...form.getInputProps("password")}
|
{...form.getInputProps("password")}
|
||||||
/>
|
/>
|
||||||
<Button fullWidth mt="xl" type="submit" loading={isLoading}>
|
<Button fullWidth mt="xl" type="submit" loading={isPending}>
|
||||||
{t("loginSubmit")}
|
{t("loginSubmit")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -2,17 +2,16 @@ import { Grid, Button } from "@mantine/core";
|
|||||||
import { GithubIcon } from "../../icons/github";
|
import { GithubIcon } from "../../icons/github";
|
||||||
import { GoogleIcon } from "../../icons/google";
|
import { GoogleIcon } from "../../icons/google";
|
||||||
import { OAuthIcon } from "../../icons/oauth";
|
import { OAuthIcon } from "../../icons/oauth";
|
||||||
import { TailscaleIcon } from "../../icons/tailscale";
|
|
||||||
|
|
||||||
interface OAuthButtonsProps {
|
interface OAuthButtonsProps {
|
||||||
oauthProviders: string[];
|
oauthProviders: string[];
|
||||||
isLoading: boolean;
|
isPending: boolean;
|
||||||
mutate: (provider: string) => void;
|
mutate: (provider: string) => void;
|
||||||
genericName: string;
|
genericName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OAuthButtons = (props: OAuthButtonsProps) => {
|
export const OAuthButtons = (props: OAuthButtonsProps) => {
|
||||||
const { oauthProviders, isLoading, genericName, mutate } = props;
|
const { oauthProviders, isPending, genericName, mutate } = props;
|
||||||
return (
|
return (
|
||||||
<Grid mb="md" mt="md" align="center" justify="center">
|
<Grid mb="md" mt="md" align="center" justify="center">
|
||||||
{oauthProviders.includes("google") && (
|
{oauthProviders.includes("google") && (
|
||||||
@@ -22,7 +21,7 @@ export const OAuthButtons = (props: OAuthButtonsProps) => {
|
|||||||
leftSection={<GoogleIcon style={{ width: 14, height: 14 }} />}
|
leftSection={<GoogleIcon style={{ width: 14, height: 14 }} />}
|
||||||
variant="default"
|
variant="default"
|
||||||
onClick={() => mutate("google")}
|
onClick={() => mutate("google")}
|
||||||
loading={isLoading}
|
loading={isPending}
|
||||||
>
|
>
|
||||||
Google
|
Google
|
||||||
</Button>
|
</Button>
|
||||||
@@ -35,25 +34,12 @@ export const OAuthButtons = (props: OAuthButtonsProps) => {
|
|||||||
leftSection={<GithubIcon style={{ width: 14, height: 14 }} />}
|
leftSection={<GithubIcon style={{ width: 14, height: 14 }} />}
|
||||||
variant="default"
|
variant="default"
|
||||||
onClick={() => mutate("github")}
|
onClick={() => mutate("github")}
|
||||||
loading={isLoading}
|
loading={isPending}
|
||||||
>
|
>
|
||||||
Github
|
Github
|
||||||
</Button>
|
</Button>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
)}
|
)}
|
||||||
{oauthProviders.includes("tailscale") && (
|
|
||||||
<Grid.Col span="content">
|
|
||||||
<Button
|
|
||||||
radius="xl"
|
|
||||||
leftSection={<TailscaleIcon style={{ width: 14, height: 14 }} />}
|
|
||||||
variant="default"
|
|
||||||
onClick={() => mutate("tailscale")}
|
|
||||||
loading={isLoading}
|
|
||||||
>
|
|
||||||
Tailscale
|
|
||||||
</Button>
|
|
||||||
</Grid.Col>
|
|
||||||
)}
|
|
||||||
{oauthProviders.includes("generic") && (
|
{oauthProviders.includes("generic") && (
|
||||||
<Grid.Col span="content">
|
<Grid.Col span="content">
|
||||||
<Button
|
<Button
|
||||||
@@ -61,7 +47,7 @@ export const OAuthButtons = (props: OAuthButtonsProps) => {
|
|||||||
leftSection={<OAuthIcon style={{ width: 14, height: 14 }} />}
|
leftSection={<OAuthIcon style={{ width: 14, height: 14 }} />}
|
||||||
variant="default"
|
variant="default"
|
||||||
onClick={() => mutate("generic")}
|
onClick={() => mutate("generic")}
|
||||||
loading={isLoading}
|
loading={isPending}
|
||||||
>
|
>
|
||||||
{genericName}
|
{genericName}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ type FormValues = z.infer<typeof schema>;
|
|||||||
|
|
||||||
interface TotpFormProps {
|
interface TotpFormProps {
|
||||||
onSubmit: (values: FormValues) => void;
|
onSubmit: (values: FormValues) => void;
|
||||||
isLoading: boolean;
|
isPending: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TotpForm = (props: TotpFormProps) => {
|
export const TotpForm = (props: TotpFormProps) => {
|
||||||
const { onSubmit, isLoading } = props;
|
const { onSubmit, isPending } = props;
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
mode: "uncontrolled",
|
mode: "uncontrolled",
|
||||||
@@ -32,7 +32,7 @@ export const TotpForm = (props: TotpFormProps) => {
|
|||||||
placeholder=""
|
placeholder=""
|
||||||
{...form.getInputProps("code")}
|
{...form.getInputProps("code")}
|
||||||
/>
|
/>
|
||||||
<Button type="submit" mt="xl" loading={isLoading} fullWidth>
|
<Button type="submit" mt="xl" loading={isPending} fullWidth>
|
||||||
Verify
|
Verify
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import React, { createContext, useContext } from "react";
|
import React, { createContext, useContext } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { AppContextSchemaType } from "../schemas/app-context-schema";
|
import { AppContextSchemaType } from "../schemas/app-context-schema";
|
||||||
@@ -14,7 +14,7 @@ export const AppContextProvider = ({
|
|||||||
data: userContext,
|
data: userContext,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
} = useQuery({
|
} = useSuspenseQuery({
|
||||||
queryKey: ["appContext"],
|
queryKey: ["appContext"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await axios.get("/api/app");
|
const res = await axios.get("/api/app");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import React, { createContext, useContext } from "react";
|
import React, { createContext, useContext } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { UserContextSchemaType } from "../schemas/user-context-schema";
|
import { UserContextSchemaType } from "../schemas/user-context-schema";
|
||||||
@@ -14,7 +14,7 @@ export const UserContextProvider = ({
|
|||||||
data: userContext,
|
data: userContext,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
} = useQuery({
|
} = useSuspenseQuery({
|
||||||
queryKey: ["userContext"],
|
queryKey: ["userContext"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await axios.get("/api/user");
|
const res = await axios.get("/api/user");
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
import type { SVGProps } from "react";
|
|
||||||
|
|
||||||
export function TailscaleIcon(props: SVGProps<SVGSVGElement>) {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<style>{".st0{opacity:0.2;fill:#CCCAC9;}.st1{fill:#FFFFFF;}"}</style>
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<path
|
|
||||||
className="st0"
|
|
||||||
d="M65.6,127.7c35.3,0,63.9-28.6,63.9-63.9S100.9,0,65.6,0S1.8,28.6,1.8,63.9S30.4,127.7,65.6,127.7z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
className="st1"
|
|
||||||
d="M65.6,318.1c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9S1.8,219,1.8,254.2S30.4,318.1,65.6,318.1z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
className="st0"
|
|
||||||
d="M65.6,512c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9S1.8,412.9,1.8,448.1S30.4,512,65.6,512z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
className="st1"
|
|
||||||
d="M257.2,318.1c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9s-63.9,28.6-63.9,63.9S221.9,318.1,257.2,318.1z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
className="st1"
|
|
||||||
d="M257.2,512c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9s-63.9,28.6-63.9,63.9S221.9,512,257.2,512z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
className="st0"
|
|
||||||
d="M257.2,127.7c35.3,0,63.9-28.6,63.9-63.9S292.5,0,257.2,0s-63.9,28.6-63.9,63.9S221.9,127.7,257.2,127.7z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
className="st0"
|
|
||||||
d="M446.4,127.7c35.3,0,63.9-28.6,63.9-63.9S481.6,0,446.4,0c-35.3,0-63.9,28.6-63.9,63.9S411.1,127.7,446.4,127.7z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
className="st1"
|
|
||||||
d="M446.4,318.1c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9s-63.9,28.6-63.9,63.9S411.1,318.1,446.4,318.1z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
className="st0"
|
|
||||||
d="M446.4,512c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9s-63.9,28.6-63.9,63.9S411.1,512,446.4,512z"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
4
frontend/src/index.css
Normal file
4
frontend/src/index.css
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
span,
|
||||||
|
p {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
26
frontend/src/lib/hooks/use-is-mounted.ts
Normal file
26
frontend/src/lib/hooks/use-is-mounted.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { useCallback, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook that determines if the component is currently mounted.
|
||||||
|
* @returns {() => boolean} A function that returns a boolean value indicating whether the component is mounted.
|
||||||
|
* @public
|
||||||
|
* @see [Documentation](https://usehooks-ts.com/react-hook/use-is-mounted)
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* const isComponentMounted = useIsMounted();
|
||||||
|
* // Use isComponentMounted() to check if the component is currently mounted before performing certain actions.
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function useIsMounted(): () => boolean {
|
||||||
|
const isMounted = useRef(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
isMounted.current = true
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted.current = false
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return useCallback(() => isMounted.current, [])
|
||||||
|
}
|
||||||
@@ -5,6 +5,20 @@ import ChainedBackend from "i18next-chained-backend";
|
|||||||
import resourcesToBackend from "i18next-resources-to-backend";
|
import resourcesToBackend from "i18next-resources-to-backend";
|
||||||
import HttpBackend from "i18next-http-backend";
|
import HttpBackend from "i18next-http-backend";
|
||||||
|
|
||||||
|
const backends = [
|
||||||
|
HttpBackend,
|
||||||
|
resourcesToBackend(
|
||||||
|
(language: string) => import(`./locales/${language}.json`),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
const backendOptions = [
|
||||||
|
{
|
||||||
|
loadPath: "https://cdn.tinyauth.app/i18n/v1/{{lng}}.json",
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
|
||||||
i18n
|
i18n
|
||||||
.use(ChainedBackend)
|
.use(ChainedBackend)
|
||||||
.use(LanguageDetector)
|
.use(LanguageDetector)
|
||||||
@@ -20,17 +34,8 @@ i18n
|
|||||||
load: "currentOnly",
|
load: "currentOnly",
|
||||||
|
|
||||||
backend: {
|
backend: {
|
||||||
backends: [
|
backends: import.meta.env.MODE !== "development" ? backends : backends.reverse(),
|
||||||
HttpBackend,
|
backendOptions: import.meta.env.MODE !== "development" ? backendOptions : backendOptions.reverse()
|
||||||
resourcesToBackend(
|
|
||||||
(language: string) => import(`./locales/${language}.json`),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
backendOptions: [
|
|
||||||
{
|
|
||||||
loadPath: "https://cdn.tinyauth.app/i18n/{{lng}}.json",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,10 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel"
|
||||||
}
|
}
|
||||||
@@ -1,45 +1,49 @@
|
|||||||
{
|
{
|
||||||
"loginTitle": "Welcome back, login with",
|
"loginTitle": "مرحبا بعودتك، قم بتسجيل الدخول باستخدام",
|
||||||
"loginDivider": "Or continue with password",
|
"loginDivider": "أو المتابعة بكلمة المرور",
|
||||||
"loginUsername": "Username",
|
"loginUsername": "اسم المستخدم",
|
||||||
"loginPassword": "Password",
|
"loginPassword": "كلمة المرور",
|
||||||
"loginSubmit": "Login",
|
"loginSubmit": "تسجيل الدخول",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "فشل تسجيل الدخول",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "الرجاء التحقق من اسم المستخدم وكلمة المرور",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginFailRateLimit": "فشلت في تسجيل الدخول عدة مرات، الرجاء المحاولة مرة أخرى لاحقا",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessTitle": "تم تسجيل الدخول",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginSuccessSubtitle": "مرحبا بعودتك!",
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
"loginOauthFailTitle": "خطأ داخلي",
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
"loginOauthFailSubtitle": "فشل في الحصول على رابط OAuth",
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
"loginOauthSuccessTitle": "إعادة توجيه",
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
"loginOauthSuccessSubtitle": "إعادة توجيه إلى مزود OAuth الخاص بك",
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
"continueRedirectingTitle": "إعادة توجيه...",
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
"continueRedirectingSubtitle": "يجب إعادة توجيهك إلى التطبيق قريبا",
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
"continueInvalidRedirectTitle": "إعادة توجيه غير صالحة",
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
"continueInvalidRedirectSubtitle": "رابط إعادة التوجيه غير صالح",
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
"continueInsecureRedirectTitle": "إعادة توجيه غير آمنة",
|
||||||
"continueTitle": "Continue",
|
"continueInsecureRedirectSubtitle": "أنت تحاول إعادة التوجيه من <Code>https</Code> إلى <Code>http</Code>، هل أنت متأكد أنك تريد المتابعة؟",
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
"continueTitle": "متابعة",
|
||||||
"internalErrorTitle": "Internal Server Error",
|
"continueSubtitle": "انقر الزر للمتابعة إلى التطبيق الخاص بك.",
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
"internalErrorTitle": "خطأ داخلي في الخادم",
|
||||||
"internalErrorButton": "Try again",
|
"internalErrorSubtitle": "حدث خطأ على الخادم ولا يمكن حاليا تلبية طلبك.",
|
||||||
"logoutFailTitle": "Failed to log out",
|
"internalErrorButton": "حاول مجددا",
|
||||||
"logoutFailSubtitle": "Please try again",
|
"logoutFailTitle": "فشل تسجيل الخروج",
|
||||||
"logoutSuccessTitle": "Logged out",
|
"logoutFailSubtitle": "يرجى إعادة المحاولة",
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
"logoutSuccessTitle": "تم تسجيل الخروج",
|
||||||
"logoutTitle": "Logout",
|
"logoutSuccessSubtitle": "تم تسجيل خروجك",
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
"logoutTitle": "تسجيل الخروج",
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
"logoutUsernameSubtitle": "أنت حاليا مسجل الدخول ك <Code>{{username}}</Code>، انقر الزر أدناه لتسجيل الخروج.",
|
||||||
"notFoundTitle": "Page not found",
|
"logoutOauthSubtitle": "أنت حاليا مسجل الدخول ك <Code>{{username}}</Code> باستخدام مزود OAuth {{provider}} ، انقر الزر أدناه لتسجيل الخروج.",
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
"notFoundTitle": "الصفحة غير موجودة",
|
||||||
"notFoundButton": "Go home",
|
"notFoundSubtitle": "الصفحة التي تبحث عنها غير موجودة.",
|
||||||
"totpFailTitle": "Failed to verify code",
|
"notFoundButton": "انتقل إلى الرئيسية",
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
"totpFailTitle": "فشل في التحقق من الرمز",
|
||||||
"totpSuccessTitle": "Verified",
|
"totpFailSubtitle": "الرجاء التحقق من الرمز الخاص بك وحاول مرة أخرى",
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessTitle": "تم التحقق",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpSuccessSubtitle": "إعادة توجيه إلى تطبيقك",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"totpTitle": "أدخل رمز TOTP الخاص بك",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedTitle": "غير مرخص",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unauthorizedResourceSubtitle": "المستخدم الذي يحمل اسم المستخدم <Code>{{username}}</Code> غير مصرح له بالوصول إلى المورد <Code>{{resource}}</Code>.",
|
||||||
"unauthorizedButton": "Try again"
|
"unaothorizedLoginSubtitle": "المستخدم الذي يحمل اسم المستخدم <Code>{{username}}</Code> غير مصرح له بتسجيل الدخول.",
|
||||||
|
"unauthorizedButton": "حاول مجددا",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "إلغاء"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,10 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,10 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,10 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel"
|
||||||
}
|
}
|
||||||
@@ -1,45 +1,49 @@
|
|||||||
{
|
{
|
||||||
"loginTitle": "Welcome back, login with",
|
"loginTitle": "Willkommen zurück, logge dich ein mit",
|
||||||
"loginDivider": "Or continue with password",
|
"loginDivider": "Oder mit Passwort fortfahren",
|
||||||
"loginUsername": "Username",
|
"loginUsername": "Benutzername",
|
||||||
"loginPassword": "Password",
|
"loginPassword": "Passwort",
|
||||||
"loginSubmit": "Login",
|
"loginSubmit": "Anmelden",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Login fehlgeschlagen",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Bitte überprüfe deinen Benutzernamen und Passwort",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginFailRateLimit": "Sie konnten sich zu oft nicht einloggen, bitte versuchen Sie es später erneut",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessTitle": "Angemeldet",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginSuccessSubtitle": "Willkommen zurück!",
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
"loginOauthFailTitle": "Interner Fehler",
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
"loginOauthFailSubtitle": "Fehler beim Abrufen der OAuth-URL",
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
"loginOauthSuccessTitle": "Leite weiter",
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
"loginOauthSuccessSubtitle": "Weiterleitung zu Ihrem OAuth-Provider",
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
"continueRedirectingTitle": "Leite weiter...",
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
"continueRedirectingSubtitle": "Sie sollten in Kürze zur App weitergeleitet werden",
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
"continueInvalidRedirectTitle": "Ungültige Weiterleitung",
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
"continueInvalidRedirectSubtitle": "Die Weiterleitungs-URL ist ungültig",
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
"continueInsecureRedirectTitle": "Unsichere Weiterleitung",
|
||||||
"continueTitle": "Continue",
|
"continueInsecureRedirectSubtitle": "Sie versuchen von <Code>https</Code> auf <Code>http</Code>weiterzuleiten. Sind Sie sicher, dass Sie fortfahren möchten?",
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
"continueTitle": "Weiter",
|
||||||
"internalErrorTitle": "Internal Server Error",
|
"continueSubtitle": "Klicken Sie auf den Button, um zur App zu gelangen.",
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
"internalErrorTitle": "Interner Serverfehler",
|
||||||
"internalErrorButton": "Try again",
|
"internalErrorSubtitle": "Ein Error ist auf dem Server aufgetreten, weshalb ihre Anfrage derzeit nicht bearbeitet werden kann.",
|
||||||
"logoutFailTitle": "Failed to log out",
|
"internalErrorButton": "Erneut versuchen",
|
||||||
"logoutFailSubtitle": "Please try again",
|
"logoutFailTitle": "Abmelden fehlgeschlagen",
|
||||||
"logoutSuccessTitle": "Logged out",
|
"logoutFailSubtitle": "Bitte versuchen Sie es erneut",
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
"logoutSuccessTitle": "Abgemeldet",
|
||||||
"logoutTitle": "Logout",
|
"logoutSuccessSubtitle": "Sie wurden abgemeldet",
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
"logoutTitle": "Abmelden",
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
"logoutUsernameSubtitle": "Sie sind derzeit als <Code>{{username}}</Code>angemeldet. Klicken Sie auf den Button unten, um sich abzumelden.",
|
||||||
"notFoundTitle": "Page not found",
|
"logoutOauthSubtitle": "Sie sind derzeit als <Code>{{username}}</Code> mit dem {{provider}} OAuth-Anbieter angemeldet. Klicken Sie auf den Button unten, um sich abzumelden.",
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
"notFoundTitle": "Seite nicht gefunden",
|
||||||
"notFoundButton": "Go home",
|
"notFoundSubtitle": "Die gesuchte Seite existiert nicht.",
|
||||||
"totpFailTitle": "Failed to verify code",
|
"notFoundButton": "Nach Hause",
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
"totpFailTitle": "Fehler beim Verifizieren des Codes",
|
||||||
"totpSuccessTitle": "Verified",
|
"totpFailSubtitle": "Bitte überprüfen Sie Ihren Code und versuchen Sie es erneut",
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessTitle": "Verifiziert",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpSuccessSubtitle": "Leite zur App weiter",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"totpTitle": "Geben Sie Ihren TOTP Code ein",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedTitle": "Unautorisiert",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unauthorizedResourceSubtitle": "Der Benutzer mit Benutzername <Code>{{username}}</Code> ist nicht berechtigt auf die Ressource <Code>{{resource}}</Code> zuzugreifen.",
|
||||||
"unauthorizedButton": "Try again"
|
"unaothorizedLoginSubtitle": "Der Benutzer mit dem Benutzernamen <Code>{{username}}</Code> ist nicht berechtigt, sich einzuloggen.",
|
||||||
|
"unauthorizedButton": "Erneut versuchen",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Είσοδος",
|
"loginSubmit": "Είσοδος",
|
||||||
"loginFailTitle": "Αποτυχία σύνδεσης",
|
"loginFailTitle": "Αποτυχία σύνδεσης",
|
||||||
"loginFailSubtitle": "Παρακαλώ ελέγξτε το όνομα χρήστη και τον κωδικό πρόσβασης",
|
"loginFailSubtitle": "Παρακαλώ ελέγξτε το όνομα χρήστη και τον κωδικό πρόσβασης",
|
||||||
|
"loginFailRateLimit": "Αποτύχατε να συνδεθείτε πάρα πολλές φορές, παρακαλώ προσπαθήστε ξανά αργότερα",
|
||||||
"loginSuccessTitle": "Συνδεδεμένος",
|
"loginSuccessTitle": "Συνδεδεμένος",
|
||||||
"loginSuccessSubtitle": "Καλώς ήρθατε!",
|
"loginSuccessSubtitle": "Καλώς ήρθατε!",
|
||||||
"loginOauthFailTitle": "Εσωτερικό σφάλμα",
|
"loginOauthFailTitle": "Εσωτερικό σφάλμα",
|
||||||
@@ -39,7 +40,10 @@
|
|||||||
"totpSuccessSubtitle": "Ανακατεύθυνση στην εφαρμογή σας",
|
"totpSuccessSubtitle": "Ανακατεύθυνση στην εφαρμογή σας",
|
||||||
"totpTitle": "Εισάγετε τον κωδικό TOTP",
|
"totpTitle": "Εισάγετε τον κωδικό TOTP",
|
||||||
"unauthorizedTitle": "Μη εξουσιοδοτημένο",
|
"unauthorizedTitle": "Μη εξουσιοδοτημένο",
|
||||||
"unauthorizedResourceSubtitle": "Ο χρήστης με όνομα χρήστη {{username}} δεν είναι εξουσιοδοτημένος να έχει πρόσβαση στον πόρο <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "Ο χρήστης με όνομα χρήστη <Code>{{username}}</Code> δεν έχει άδεια πρόσβασης στον πόρο <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "Ο χρήστης με όνομα χρήστη {{username}} δεν είναι εξουσιοδοτημένος να συνδεθεί.",
|
"unaothorizedLoginSubtitle": "Ο χρήστης με όνομα χρήστη <Code>{{username}}</Code> δεν είναι εξουσιοδοτημένος να συνδεθεί.",
|
||||||
"unauthorizedButton": "Προσπαθήστε ξανά"
|
"unauthorizedButton": "Προσπαθήστε ξανά",
|
||||||
|
"untrustedRedirectTitle": "Μη έμπιστη ανακατεύθυνση",
|
||||||
|
"untrustedRedirectSubtitle": "Προσπαθείτε να ανακατευθύνετε σε έναν τομέα που δεν ταιριάζει με τον ρυθμισμένο τομέα σας (<Code>{{domain}}</Code>). Είστε βέβαιοι ότι θέλετε να συνεχίσετε;",
|
||||||
|
"cancelTitle": "Ακύρωση"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,12 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.",
|
||||||
|
"unauthorizedButton": "Try again",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel",
|
||||||
|
"forgotPasswordTitle": "Forgot your password?"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,12 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.",
|
||||||
|
"unauthorizedButton": "Try again",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel",
|
||||||
|
"forgotPasswordTitle": "Forgot your password?"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,10 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,10 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Se connecter",
|
"loginSubmit": "Se connecter",
|
||||||
"loginFailTitle": "Échec de la connexion",
|
"loginFailTitle": "Échec de la connexion",
|
||||||
"loginFailSubtitle": "Veuillez vérifier votre nom d'utilisateur et votre mot de passe",
|
"loginFailSubtitle": "Veuillez vérifier votre nom d'utilisateur et votre mot de passe",
|
||||||
|
"loginFailRateLimit": "Vous n'avez pas pu vous connecter trop de fois, veuillez réessayer plus tard",
|
||||||
"loginSuccessTitle": "Connecté",
|
"loginSuccessTitle": "Connecté",
|
||||||
"loginSuccessSubtitle": "Bienvenue!",
|
"loginSuccessSubtitle": "Bienvenue!",
|
||||||
"loginOauthFailTitle": "Erreur interne",
|
"loginOauthFailTitle": "Erreur interne",
|
||||||
@@ -39,7 +40,10 @@
|
|||||||
"totpSuccessSubtitle": "Redirection vers votre application",
|
"totpSuccessSubtitle": "Redirection vers votre application",
|
||||||
"totpTitle": "Saisissez votre code TOTP",
|
"totpTitle": "Saisissez votre code TOTP",
|
||||||
"unauthorizedTitle": "Non autorisé",
|
"unauthorizedTitle": "Non autorisé",
|
||||||
"unauthorizedResourceSubtitle": "L'utilisateur avec le nom d'utilisateur {{username}} n'est pas autorisé à accéder à la ressource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "L'utilisateur avec le nom d'utilisateur <Code>{{username}}</Code> n'est pas autorisé à accéder à la ressource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "L'utilisateur avec le nom d'utilisateur {{username}} n'est pas autorisé à se connecter.",
|
"unaothorizedLoginSubtitle": "L'utilisateur avec le nom d'utilisateur <Code>{{username}}</Code> n'est pas autorisé à se connecter.",
|
||||||
"unauthorizedButton": "Réessayer"
|
"unauthorizedButton": "Réessayer",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,10 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,10 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,10 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,10 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,10 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel"
|
||||||
}
|
}
|
||||||
@@ -1,45 +1,49 @@
|
|||||||
{
|
{
|
||||||
"loginTitle": "Welcome back, login with",
|
"loginTitle": "Welkom terug, log in met",
|
||||||
"loginDivider": "Or continue with password",
|
"loginDivider": "Of ga door met wachtwoord",
|
||||||
"loginUsername": "Username",
|
"loginUsername": "Gebruikersnaam",
|
||||||
"loginPassword": "Password",
|
"loginPassword": "Wachtwoord",
|
||||||
"loginSubmit": "Login",
|
"loginSubmit": "Log in",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Mislukt om in te loggen",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Controleer je gebruikersnaam en wachtwoord",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginFailRateLimit": "Inloggen te vaak mislukt, probeer het later opnieuw",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessTitle": "Ingelogd",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginSuccessSubtitle": "Welkom terug!",
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
"loginOauthFailTitle": "Interne fout",
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
"loginOauthFailSubtitle": "Fout bij het ophalen van OAuth URL",
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
"loginOauthSuccessTitle": "Omleiden",
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
"loginOauthSuccessSubtitle": "Omleiden naar je OAuth provider",
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
"continueRedirectingTitle": "Omleiden...",
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
"continueRedirectingSubtitle": "Je wordt naar de app doorgestuurd",
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
"continueInvalidRedirectTitle": "Ongeldige omleiding",
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
"continueInvalidRedirectSubtitle": "De omleidings-URL is ongeldig",
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
"continueInsecureRedirectTitle": "Onveilige doorverwijzing",
|
||||||
"continueTitle": "Continue",
|
"continueInsecureRedirectSubtitle": "Je probeert door te verwijzen van <Code>https</Code> naar <Code>http</Code>, weet je zeker dat je wilt doorgaan?",
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
"continueTitle": "Ga verder",
|
||||||
"internalErrorTitle": "Internal Server Error",
|
"continueSubtitle": "Klik op de knop om door te gaan naar de app.",
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
"internalErrorTitle": "Interne server fout",
|
||||||
"internalErrorButton": "Try again",
|
"internalErrorSubtitle": "Er is een fout opgetreden op de server en het kan momenteel niet voldoen aan je verzoek.",
|
||||||
"logoutFailTitle": "Failed to log out",
|
"internalErrorButton": "Opnieuw proberen",
|
||||||
"logoutFailSubtitle": "Please try again",
|
"logoutFailTitle": "Afmelden mislukt",
|
||||||
"logoutSuccessTitle": "Logged out",
|
"logoutFailSubtitle": "Probeer het opnieuw",
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
"logoutSuccessTitle": "Afgemeld",
|
||||||
"logoutTitle": "Logout",
|
"logoutSuccessSubtitle": "Je bent afgemeld",
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
"logoutTitle": "Afmelden",
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
"logoutUsernameSubtitle": "Je bent momenteel ingelogd als <Code>{{username}}</Code>, klik op de knop hieronder om uit te loggen.",
|
||||||
"notFoundTitle": "Page not found",
|
"logoutOauthSubtitle": "Je bent momenteel ingelogd als <Code>{{username}}</Code> met behulp van de {{provider}} OAuth provider, klik op de knop hieronder om uit te loggen.",
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
"notFoundTitle": "Pagina niet gevonden",
|
||||||
"notFoundButton": "Go home",
|
"notFoundSubtitle": "De pagina die je zoekt bestaat niet.",
|
||||||
"totpFailTitle": "Failed to verify code",
|
"notFoundButton": "Naar startpagina",
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
"totpFailTitle": "Verifiëren van code mislukt",
|
||||||
"totpSuccessTitle": "Verified",
|
"totpFailSubtitle": "Controleer je code en probeer het opnieuw",
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessTitle": "Geverifiëerd",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpSuccessSubtitle": "Omleiden naar je app",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"totpTitle": "Voer je TOTP-code in",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedTitle": "Ongeautoriseerd",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unauthorizedResourceSubtitle": "De gebruiker met gebruikersnaam <Code>{{username}}</Code> heeft geen toegang tot de bron <Code>{{resource}}</Code>.",
|
||||||
"unauthorizedButton": "Try again"
|
"unaothorizedLoginSubtitle": "De gebruiker met gebruikersnaam <Code>{{username}}</Code> is niet gemachtigd om in te loggen.",
|
||||||
|
"unauthorizedButton": "Opnieuw proberen",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,10 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel"
|
||||||
}
|
}
|
||||||
@@ -3,9 +3,10 @@
|
|||||||
"loginDivider": "Lub kontynuuj z hasłem",
|
"loginDivider": "Lub kontynuuj z hasłem",
|
||||||
"loginUsername": "Nazwa użytkownika",
|
"loginUsername": "Nazwa użytkownika",
|
||||||
"loginPassword": "Hasło",
|
"loginPassword": "Hasło",
|
||||||
"loginSubmit": "Login",
|
"loginSubmit": "Zaloguj się",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Nie udało się zalogować",
|
||||||
"loginFailSubtitle": "Sprawdź swoją nazwę użytkownika i hasło",
|
"loginFailSubtitle": "Sprawdź swoją nazwę użytkownika i hasło",
|
||||||
|
"loginFailRateLimit": "Nie udało się zalogować zbyt wiele razy, spróbuj ponownie później",
|
||||||
"loginSuccessTitle": "Zalogowano",
|
"loginSuccessTitle": "Zalogowano",
|
||||||
"loginSuccessSubtitle": "Witaj ponownie!",
|
"loginSuccessSubtitle": "Witaj ponownie!",
|
||||||
"loginOauthFailTitle": "Wewnętrzny błąd",
|
"loginOauthFailTitle": "Wewnętrzny błąd",
|
||||||
@@ -21,9 +22,9 @@
|
|||||||
"continueTitle": "Kontynuuj",
|
"continueTitle": "Kontynuuj",
|
||||||
"continueSubtitle": "Kliknij przycisk, aby przejść do aplikacji.",
|
"continueSubtitle": "Kliknij przycisk, aby przejść do aplikacji.",
|
||||||
"internalErrorTitle": "Wewnętrzny błąd serwera",
|
"internalErrorTitle": "Wewnętrzny błąd serwera",
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
"internalErrorSubtitle": "Wystąpił błąd na serwerze i obecnie nie można obsłużyć tego żądania.",
|
||||||
"internalErrorButton": "Spróbuj ponownie",
|
"internalErrorButton": "Spróbuj ponownie",
|
||||||
"logoutFailTitle": "Failed to log out",
|
"logoutFailTitle": "Nie udało się wylogować",
|
||||||
"logoutFailSubtitle": "Spróbuj ponownie",
|
"logoutFailSubtitle": "Spróbuj ponownie",
|
||||||
"logoutSuccessTitle": "Wylogowano",
|
"logoutSuccessTitle": "Wylogowano",
|
||||||
"logoutSuccessSubtitle": "Zostałeś wylogowany",
|
"logoutSuccessSubtitle": "Zostałeś wylogowany",
|
||||||
@@ -39,7 +40,10 @@
|
|||||||
"totpSuccessSubtitle": "Przekierowywanie do aplikacji",
|
"totpSuccessSubtitle": "Przekierowywanie do aplikacji",
|
||||||
"totpTitle": "Wprowadź kod TOTP",
|
"totpTitle": "Wprowadź kod TOTP",
|
||||||
"unauthorizedTitle": "Nieautoryzowany",
|
"unauthorizedTitle": "Nieautoryzowany",
|
||||||
"unauthorizedResourceSubtitle": "Użytkownik o nazwie {{username}} nie jest upoważniony do uzyskania dostępu do zasobu <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "Użytkownik o nazwie <Code>{{username}}</Code> nie jest upoważniony do uzyskania dostępu do zasobu <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "Użytkownik o nazwie {{username}} nie jest upoważniony do logowania.",
|
"unaothorizedLoginSubtitle": "Użytkownik o nazwie <Code>{{username}}</Code> nie jest upoważniony do logowania.",
|
||||||
"unauthorizedButton": "Spróbuj ponownie"
|
"unauthorizedButton": "Spróbuj ponownie",
|
||||||
|
"untrustedRedirectTitle": "Niezaufane przekierowanie",
|
||||||
|
"untrustedRedirectSubtitle": "Próbujesz przekierować do domeny, która nie pasuje do skonfigurowanej przez Ciebie domeny (<Code>{{domain}}</Code>). Czy na pewno chcesz kontynuować?",
|
||||||
|
"cancelTitle": "Anuluj"
|
||||||
}
|
}
|
||||||
@@ -1,45 +1,49 @@
|
|||||||
{
|
{
|
||||||
"loginTitle": "Welcome back, login with",
|
"loginTitle": "Bem-vindo de volta, faça o login com",
|
||||||
"loginDivider": "Or continue with password",
|
"loginDivider": "Ou continuar com uma senha",
|
||||||
"loginUsername": "Username",
|
"loginUsername": "Nome de usuário",
|
||||||
"loginPassword": "Password",
|
"loginPassword": "Senha",
|
||||||
"loginSubmit": "Login",
|
"loginSubmit": "Entrar",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Falha ao iniciar sessão",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Por favor, verifique seu usuário e senha",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginFailRateLimit": "Você falhou em iniciar sessão muitas vezes, por favor tente novamente mais tarde",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessTitle": "Sessão Iniciada",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginSuccessSubtitle": "Bem-vindo de volta!",
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
"loginOauthFailTitle": "Erro interno",
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
"loginOauthFailSubtitle": "Falha ao obter URL de OAuth",
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
"loginOauthSuccessTitle": "Redirecionando",
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
"loginOauthSuccessSubtitle": "Redirecionando para seu provedor OAuth",
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
"continueRedirectingTitle": "Redirecionando...",
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
"continueRedirectingSubtitle": "Você deve ser redirecionado para o aplicativo em breve",
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
"continueInvalidRedirectTitle": "Redirecionamento inválido",
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
"continueInvalidRedirectSubtitle": "O endereço de redirecionamento é inválido",
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
"continueInsecureRedirectTitle": "Redirecionamento inseguro",
|
||||||
"continueTitle": "Continue",
|
"continueInsecureRedirectSubtitle": "Você está tentando redirecionar de <Code>https</Code> para <Code>http</Code>, você tem certeza que deseja continuar?",
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
"continueTitle": "Continuar",
|
||||||
"internalErrorTitle": "Internal Server Error",
|
"continueSubtitle": "Clique no botão para continuar para o seu aplicativo.",
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
"internalErrorTitle": "Erro interno do servidor",
|
||||||
"internalErrorButton": "Try again",
|
"internalErrorSubtitle": "Ocorreu um erro no servidor e atualmente não pode servir sua solicitação.",
|
||||||
"logoutFailTitle": "Failed to log out",
|
"internalErrorButton": "Tentar novamente",
|
||||||
"logoutFailSubtitle": "Please try again",
|
"logoutFailTitle": "Falha ao encerrar sessão",
|
||||||
"logoutSuccessTitle": "Logged out",
|
"logoutFailSubtitle": "Por favor, tente novamente",
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
"logoutSuccessTitle": "Sessão encerrada",
|
||||||
"logoutTitle": "Logout",
|
"logoutSuccessSubtitle": "Você foi desconectado",
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
"logoutTitle": "Sair",
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
"logoutUsernameSubtitle": "Você está atualmente logado como <Code>{{username}}</Code>, clique no botão abaixo para sair.",
|
||||||
"notFoundTitle": "Page not found",
|
"logoutOauthSubtitle": "Você está atualmente logado como <Code>{{username}}</Code> usando o provedor {{provider}} OAuth, clique no botão abaixo para sair.",
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
"notFoundTitle": "Página não encontrada",
|
||||||
"notFoundButton": "Go home",
|
"notFoundSubtitle": "A página que você está procurando não existe.",
|
||||||
"totpFailTitle": "Failed to verify code",
|
"notFoundButton": "Voltar para a tela inicial",
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
"totpFailTitle": "Falha ao verificar código",
|
||||||
"totpSuccessTitle": "Verified",
|
"totpFailSubtitle": "Por favor, verifique seu código e tente novamente",
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessTitle": "Verificado",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpSuccessSubtitle": "Redirecionando para o seu aplicativo",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"totpTitle": "Insira o seu código TOTP",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedTitle": "Não autorizado",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unauthorizedResourceSubtitle": "O usuário com nome de usuário <Code>{{username}}</Code> não está autorizado a acessar o recurso <Code>{{resource}}</Code>.",
|
||||||
"unauthorizedButton": "Try again"
|
"unaothorizedLoginSubtitle": "O usuário com o nome <Code>{{username}}</Code> não está autorizado a acessar.",
|
||||||
|
"unauthorizedButton": "Tentar novamente",
|
||||||
|
"untrustedRedirectTitle": "Redirecionamento não confiável",
|
||||||
|
"untrustedRedirectSubtitle": "Você está tentando redirecionar para um domínio que não corresponde ao seu domínio configurado (<Code>{{domain}}</Code>). Tem certeza que deseja continuar?",
|
||||||
|
"cancelTitle": "Cancelar"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,10 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,10 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,10 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,10 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,10 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel"
|
||||||
}
|
}
|
||||||
@@ -1,45 +1,49 @@
|
|||||||
{
|
{
|
||||||
"loginTitle": "Welcome back, login with",
|
"loginTitle": "Welcome back, login with",
|
||||||
"loginDivider": "Or continue with password",
|
"loginDivider": "Ya da şifre ile devam edin",
|
||||||
"loginUsername": "Username",
|
"loginUsername": "Kullanıcı Adı",
|
||||||
"loginPassword": "Password",
|
"loginPassword": "Şifre",
|
||||||
"loginSubmit": "Login",
|
"loginSubmit": "Giriş Yap",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Giriş yapılamadı",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessTitle": "Giriş yapıldı",
|
||||||
|
"loginSuccessSubtitle": "Tekrar hoş geldiniz!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
"loginOauthSuccessTitle": "Yönlendiriliyor",
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
"continueRedirectingTitle": "Yönlendiriliyor...",
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
"continueInvalidRedirectTitle": "Invalid redirect",
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
"continueInsecureRedirectTitle": "Insecure redirect",
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
||||||
"continueTitle": "Continue",
|
"continueTitle": "Devam et",
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
"continueSubtitle": "Click the button to continue to your app.",
|
||||||
"internalErrorTitle": "Internal Server Error",
|
"internalErrorTitle": "İç Sunucu Hatası",
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
||||||
"internalErrorButton": "Try again",
|
"internalErrorButton": "Tekrar deneyin",
|
||||||
"logoutFailTitle": "Failed to log out",
|
"logoutFailTitle": "Failed to log out",
|
||||||
"logoutFailSubtitle": "Please try again",
|
"logoutFailSubtitle": "Lütfen tekrar deneyin",
|
||||||
"logoutSuccessTitle": "Logged out",
|
"logoutSuccessTitle": "Çıkış yapıldı",
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
"logoutSuccessSubtitle": "You have been logged out",
|
||||||
"logoutTitle": "Logout",
|
"logoutTitle": "Logout",
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
||||||
"notFoundTitle": "Page not found",
|
"notFoundTitle": "Sayfa bulunamadı",
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
"notFoundSubtitle": "Aradığınız sayfa mevcut değil.",
|
||||||
"notFoundButton": "Go home",
|
"notFoundButton": "Ana sayfaya git",
|
||||||
"totpFailTitle": "Failed to verify code",
|
"totpFailTitle": "Kod doğrulanamadı",
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
"totpFailSubtitle": "Please check your code and try again",
|
||||||
"totpSuccessTitle": "Verified",
|
"totpSuccessTitle": "Doğrulandı",
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "İptal"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,10 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,10 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel"
|
||||||
}
|
}
|
||||||
@@ -1,45 +1,49 @@
|
|||||||
{
|
{
|
||||||
"loginTitle": "Welcome back, login with",
|
"loginTitle": "欢迎回来,请登录",
|
||||||
"loginDivider": "Or continue with password",
|
"loginDivider": "或者继续使用密码",
|
||||||
"loginUsername": "Username",
|
"loginUsername": "用户名",
|
||||||
"loginPassword": "Password",
|
"loginPassword": "密码",
|
||||||
"loginSubmit": "Login",
|
"loginSubmit": "登录",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "登录失败",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "请检查您的用户名和密码",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginFailRateLimit": "您登录次数过多,请稍后再试",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessTitle": "已登录",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginSuccessSubtitle": "欢迎回来!",
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
"loginOauthFailTitle": "内部错误",
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
"loginOauthFailSubtitle": "获取 OAuth URL 失败",
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
"loginOauthSuccessTitle": "重定向中",
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
"loginOauthSuccessSubtitle": "重定向到您的 OAuth 提供商",
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
"continueRedirectingTitle": "正在重定向……",
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
"continueRedirectingSubtitle": "您应该很快被重定向到应用",
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
"continueInvalidRedirectTitle": "无效的重定向",
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
"continueInvalidRedirectSubtitle": "重定向URL无效",
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
"continueInsecureRedirectTitle": "不安全的重定向",
|
||||||
"continueTitle": "Continue",
|
"continueInsecureRedirectSubtitle": "您正在尝试将 <Code>https</Code> 重定向到 <Code>http</Code>,您确定要继续吗?",
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
"continueTitle": "继续",
|
||||||
"internalErrorTitle": "Internal Server Error",
|
"continueSubtitle": "点击按钮以继续您的应用。",
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
"internalErrorTitle": "服务器内部错误",
|
||||||
"internalErrorButton": "Try again",
|
"internalErrorSubtitle": "服务器上发生错误,当前无法满足您的请求。",
|
||||||
"logoutFailTitle": "Failed to log out",
|
"internalErrorButton": "重试",
|
||||||
"logoutFailSubtitle": "Please try again",
|
"logoutFailTitle": "注销失败",
|
||||||
"logoutSuccessTitle": "Logged out",
|
"logoutFailSubtitle": "请重试",
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
"logoutSuccessTitle": "已登出",
|
||||||
"logoutTitle": "Logout",
|
"logoutSuccessSubtitle": "您已登出",
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
"logoutTitle": "登出",
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
"logoutUsernameSubtitle": "您当前以 <Code>{{username}}</Code> 的身份登录,点击下方按钮退出登录。",
|
||||||
"notFoundTitle": "Page not found",
|
"logoutOauthSubtitle": "您当前以 <Code>{{username}}</Code> 的身份登录,使用的是 {{provider}} OAuth 提供商,点击下方按钮退出登录。",
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
"notFoundTitle": "无法找到页面",
|
||||||
"notFoundButton": "Go home",
|
"notFoundSubtitle": "您正在查找的页面不存在。",
|
||||||
"totpFailTitle": "Failed to verify code",
|
"notFoundButton": "回到主页",
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
"totpFailTitle": "无法验证代码",
|
||||||
"totpSuccessTitle": "Verified",
|
"totpFailSubtitle": "请检查您的代码并重试",
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessTitle": "已验证",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpSuccessSubtitle": "重定向到您的应用",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"totpTitle": "输入您的 TOTP 代码",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedTitle": "未授权",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unauthorizedResourceSubtitle": "用户 <Code>{{username}}</Code> 无权访问资源 <Code>{{resource}}</Code>。",
|
||||||
"unauthorizedButton": "Try again"
|
"unaothorizedLoginSubtitle": "用户名 <Code>{{username}}</Code> 无登录权限。",
|
||||||
|
"unauthorizedButton": "重试",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,10 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again",
|
||||||
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
|
||||||
|
"cancelTitle": "Cancel"
|
||||||
}
|
}
|
||||||
@@ -18,18 +18,14 @@ import { InternalServerError } from "./pages/internal-server-error.tsx";
|
|||||||
import { TotpPage } from "./pages/totp-page.tsx";
|
import { TotpPage } from "./pages/totp-page.tsx";
|
||||||
import { AppContextProvider } from "./context/app-context.tsx";
|
import { AppContextProvider } from "./context/app-context.tsx";
|
||||||
import "./lib/i18n/i18n.ts";
|
import "./lib/i18n/i18n.ts";
|
||||||
|
import { ForgotPasswordPage } from "./pages/forgot-password-page.tsx";
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient();
|
||||||
defaultOptions: {
|
|
||||||
queries: {
|
|
||||||
suspense: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<MantineProvider forceColorScheme="dark">
|
<MantineProvider defaultColorScheme="auto">
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Notifications />
|
<Notifications />
|
||||||
<AppContextProvider>
|
<AppContextProvider>
|
||||||
@@ -43,6 +39,10 @@ createRoot(document.getElementById("root")!).render(
|
|||||||
<Route path="/continue" element={<ContinuePage />} />
|
<Route path="/continue" element={<ContinuePage />} />
|
||||||
<Route path="/unauthorized" element={<UnauthorizedPage />} />
|
<Route path="/unauthorized" element={<UnauthorizedPage />} />
|
||||||
<Route path="/error" element={<InternalServerError />} />
|
<Route path="/error" element={<InternalServerError />} />
|
||||||
|
<Route
|
||||||
|
path="/forgot-password"
|
||||||
|
element={<ForgotPasswordPage />}
|
||||||
|
/>
|
||||||
<Route path="*" element={<NotFoundPage />} />
|
<Route path="*" element={<NotFoundPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Navigate } from "react-router";
|
|||||||
import { useUserContext } from "../context/user-context";
|
import { useUserContext } from "../context/user-context";
|
||||||
import { Layout } from "../components/layouts/layout";
|
import { Layout } from "../components/layouts/layout";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { isQueryValid } from "../utils/utils";
|
import { escapeRegex, isValidRedirectUri } from "../utils/utils";
|
||||||
import { useAppContext } from "../context/app-context";
|
import { useAppContext } from "../context/app-context";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@@ -14,14 +14,14 @@ export const ContinuePage = () => {
|
|||||||
const redirectUri = params.get("redirect_uri") ?? "";
|
const redirectUri = params.get("redirect_uri") ?? "";
|
||||||
|
|
||||||
const { isLoggedIn } = useUserContext();
|
const { isLoggedIn } = useUserContext();
|
||||||
const { disableContinue } = useAppContext();
|
const { disableContinue, domain } = useAppContext();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
return <Navigate to={`/login?redirect_uri=${redirectUri}`} />;
|
return <Navigate to={`/login?redirect_uri=${redirectUri}`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isQueryValid(redirectUri)) {
|
if (!isValidRedirectUri(redirectUri)) {
|
||||||
return <Navigate to="/" />;
|
return <Navigate to="/" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +51,35 @@ export const ContinuePage = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const regex = new RegExp(`^.*${escapeRegex(domain)}$`);
|
||||||
|
|
||||||
|
if (!regex.test(uri.hostname)) {
|
||||||
|
return (
|
||||||
|
<ContinuePageLayout>
|
||||||
|
<Text size="xl" fw={700}>
|
||||||
|
{t("untrustedRedirectTitle")}
|
||||||
|
</Text>
|
||||||
|
<Trans
|
||||||
|
i18nKey="untrustedRedirectSubtitle"
|
||||||
|
t={t}
|
||||||
|
components={{ Code: <Code /> }}
|
||||||
|
values={{ domain: domain }}
|
||||||
|
/>
|
||||||
|
<Button fullWidth mt="xl" color="red" onClick={redirect}>
|
||||||
|
{t("continueTitle")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
mt="sm"
|
||||||
|
color="gray"
|
||||||
|
onClick={() => (window.location.href = "/")}
|
||||||
|
>
|
||||||
|
{t("cancelTitle")}
|
||||||
|
</Button>
|
||||||
|
</ContinuePageLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (disableContinue) {
|
if (disableContinue) {
|
||||||
window.location.href = redirectUri;
|
window.location.href = redirectUri;
|
||||||
return (
|
return (
|
||||||
@@ -79,6 +108,14 @@ export const ContinuePage = () => {
|
|||||||
<Button fullWidth mt="xl" color="yellow" onClick={redirect}>
|
<Button fullWidth mt="xl" color="yellow" onClick={redirect}>
|
||||||
{t("continueTitle")}
|
{t("continueTitle")}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
mt="sm"
|
||||||
|
color="gray"
|
||||||
|
onClick={() => (window.location.href = "/")}
|
||||||
|
>
|
||||||
|
{t("cancelTitle")}
|
||||||
|
</Button>
|
||||||
</ContinuePageLayout>
|
</ContinuePageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
25
frontend/src/pages/forgot-password-page.tsx
Normal file
25
frontend/src/pages/forgot-password-page.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Paper, Text, TypographyStylesProvider } from "@mantine/core";
|
||||||
|
import { Layout } from "../components/layouts/layout";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useAppContext } from "../context/app-context";
|
||||||
|
import Markdown from 'react-markdown'
|
||||||
|
|
||||||
|
export const ForgotPasswordPage = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { forgotPasswordMessage } = useAppContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<Paper shadow="md" p={30} mt={30} radius="md" withBorder>
|
||||||
|
<Text size="xl" fw={700}>
|
||||||
|
{t("forgotPasswordTitle")}
|
||||||
|
</Text>
|
||||||
|
<TypographyStylesProvider>
|
||||||
|
<Markdown>
|
||||||
|
{forgotPasswordMessage}
|
||||||
|
</Markdown>
|
||||||
|
</TypographyStylesProvider>
|
||||||
|
</Paper>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
import { Paper, Title, Text, Divider } from "@mantine/core";
|
import { Paper, Title, Text, Divider } from "@mantine/core";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import axios from "axios";
|
import axios, { type AxiosError } from "axios";
|
||||||
import { useUserContext } from "../context/user-context";
|
import { useUserContext } from "../context/user-context";
|
||||||
import { Navigate } from "react-router";
|
import { Navigate } from "react-router";
|
||||||
import { Layout } from "../components/layouts/layout";
|
import { Layout } from "../components/layouts/layout";
|
||||||
import { OAuthButtons } from "../components/auth/oauth-buttons";
|
import { OAuthButtons } from "../components/auth/oauth-buttons";
|
||||||
import { LoginFormValues } from "../schemas/login-schema";
|
import { LoginFormValues } from "../schemas/login-schema";
|
||||||
import { LoginForm } from "../components/auth/login-forn";
|
import { LoginForm } from "../components/auth/login-forn";
|
||||||
import { isQueryValid } from "../utils/utils";
|
|
||||||
import { useAppContext } from "../context/app-context";
|
import { useAppContext } from "../context/app-context";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useIsMounted } from "../lib/hooks/use-is-mounted";
|
||||||
|
import { isValidRedirectUri } from "../utils/utils";
|
||||||
|
|
||||||
export const LoginPage = () => {
|
export const LoginPage = () => {
|
||||||
const queryString = window.location.search;
|
const queryString = window.location.search;
|
||||||
@@ -18,22 +20,45 @@ export const LoginPage = () => {
|
|||||||
const redirectUri = params.get("redirect_uri") ?? "";
|
const redirectUri = params.get("redirect_uri") ?? "";
|
||||||
|
|
||||||
const { isLoggedIn } = useUserContext();
|
const { isLoggedIn } = useUserContext();
|
||||||
const { configuredProviders, title, genericName } = useAppContext();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const oauthProviders = configuredProviders.filter(
|
|
||||||
(value) => value !== "username",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
return <Navigate to="/logout" />;
|
return <Navigate to="/logout" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
configuredProviders,
|
||||||
|
title,
|
||||||
|
genericName,
|
||||||
|
oauthAutoRedirect: oauthAutoRedirectContext,
|
||||||
|
} = useAppContext();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [oauthAutoRedirect, setOAuthAutoRedirect] = useState(
|
||||||
|
oauthAutoRedirectContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
const oauthProviders = configuredProviders.filter(
|
||||||
|
(value) => value !== "username",
|
||||||
|
);
|
||||||
|
|
||||||
|
const isMounted = useIsMounted();
|
||||||
|
|
||||||
const loginMutation = useMutation({
|
const loginMutation = useMutation({
|
||||||
mutationFn: (login: LoginFormValues) => {
|
mutationFn: (login: LoginFormValues) => {
|
||||||
return axios.post("/api/login", login);
|
return axios.post("/api/login", login);
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (data: AxiosError) => {
|
||||||
|
if (data.response) {
|
||||||
|
if (data.response.status === 429) {
|
||||||
|
notifications.show({
|
||||||
|
title: t("loginFailTitle"),
|
||||||
|
message: t("loginFailRateLimit"),
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
notifications.show({
|
notifications.show({
|
||||||
title: t("loginFailTitle"),
|
title: t("loginFailTitle"),
|
||||||
message: t("loginFailSubtitle"),
|
message: t("loginFailSubtitle"),
|
||||||
@@ -53,7 +78,7 @@ export const LoginPage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!isQueryValid(redirectUri)) {
|
if (!isValidRedirectUri(redirectUri)) {
|
||||||
window.location.replace("/");
|
window.location.replace("/");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -75,6 +100,7 @@ export const LoginPage = () => {
|
|||||||
message: t("loginOauthFailSubtitle"),
|
message: t("loginOauthFailSubtitle"),
|
||||||
color: "red",
|
color: "red",
|
||||||
});
|
});
|
||||||
|
setOAuthAutoRedirect("none");
|
||||||
},
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
@@ -92,6 +118,33 @@ export const LoginPage = () => {
|
|||||||
loginMutation.mutate(values);
|
loginMutation.mutate(values);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isMounted()) {
|
||||||
|
if (
|
||||||
|
oauthProviders.includes(oauthAutoRedirect) &&
|
||||||
|
isValidRedirectUri(redirectUri)
|
||||||
|
) {
|
||||||
|
loginOAuthMutation.mutate(oauthAutoRedirect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (
|
||||||
|
oauthProviders.includes(oauthAutoRedirect) &&
|
||||||
|
isValidRedirectUri(redirectUri)
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<Paper shadow="md" p="xl" mt={30} radius="md" withBorder>
|
||||||
|
<Text size="xl" fw={700}>
|
||||||
|
{t("continueRedirectingTitle")}
|
||||||
|
</Text>
|
||||||
|
<Text>{t("loginOauthSuccessSubtitle")}</Text>
|
||||||
|
</Paper>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<Title ta="center">{title}</Title>
|
<Title ta="center">{title}</Title>
|
||||||
@@ -103,7 +156,7 @@ export const LoginPage = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
<OAuthButtons
|
<OAuthButtons
|
||||||
oauthProviders={oauthProviders}
|
oauthProviders={oauthProviders}
|
||||||
isLoading={loginOAuthMutation.isLoading}
|
isPending={loginOAuthMutation.isPending}
|
||||||
mutate={loginOAuthMutation.mutate}
|
mutate={loginOAuthMutation.mutate}
|
||||||
genericName={genericName}
|
genericName={genericName}
|
||||||
/>
|
/>
|
||||||
@@ -118,7 +171,7 @@ export const LoginPage = () => {
|
|||||||
)}
|
)}
|
||||||
{configuredProviders.includes("username") && (
|
{configuredProviders.includes("username") && (
|
||||||
<LoginForm
|
<LoginForm
|
||||||
isLoading={loginMutation.isLoading}
|
isPending={loginMutation.isPending}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { useAppContext } from "../context/app-context";
|
|||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export const LogoutPage = () => {
|
export const LogoutPage = () => {
|
||||||
const { isLoggedIn, username, oauth, provider } = useUserContext();
|
const { isLoggedIn, oauth, provider, email, username } = useUserContext();
|
||||||
const { genericName } = useAppContext();
|
const { genericName } = useAppContext();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ export const LogoutPage = () => {
|
|||||||
values={{
|
values={{
|
||||||
provider:
|
provider:
|
||||||
provider === "generic" ? genericName : capitalize(provider),
|
provider === "generic" ? genericName : capitalize(provider),
|
||||||
username: username,
|
username: email,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -74,7 +74,7 @@ export const LogoutPage = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
mt="xl"
|
mt="xl"
|
||||||
onClick={() => logoutMutation.mutate()}
|
onClick={() => logoutMutation.mutate()}
|
||||||
loading={logoutMutation.isLoading}
|
loading={logoutMutation.isPending}
|
||||||
>
|
>
|
||||||
{t("logoutTitle")}
|
{t("logoutTitle")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export const TotpPage = () => {
|
|||||||
{t("totpTitle")}
|
{t("totpTitle")}
|
||||||
</Text>
|
</Text>
|
||||||
<TotpForm
|
<TotpForm
|
||||||
isLoading={totpMutation.isLoading}
|
isPending={totpMutation.isPending}
|
||||||
onSubmit={(values) => totpMutation.mutate(values)}
|
onSubmit={(values) => totpMutation.mutate(values)}
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,47 +1,71 @@
|
|||||||
import { Button, Code, Paper, Text } from "@mantine/core";
|
import { Button, Code, Paper, Text } from "@mantine/core";
|
||||||
import { Layout } from "../components/layouts/layout";
|
import { Layout } from "../components/layouts/layout";
|
||||||
import { Navigate } from "react-router";
|
import { Navigate } from "react-router";
|
||||||
import { isQueryValid } from "../utils/utils";
|
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
import React from "react";
|
||||||
|
import { isValidQuery } from "../utils/utils";
|
||||||
|
|
||||||
export const UnauthorizedPage = () => {
|
export const UnauthorizedPage = () => {
|
||||||
const queryString = window.location.search;
|
const queryString = window.location.search;
|
||||||
const params = new URLSearchParams(queryString);
|
const params = new URLSearchParams(queryString);
|
||||||
const username = params.get("username") ?? "";
|
const username = params.get("username") ?? "";
|
||||||
|
const groupErr = params.get("groupErr") ?? "";
|
||||||
const resource = params.get("resource") ?? "";
|
const resource = params.get("resource") ?? "";
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!isQueryValid(username)) {
|
if (!isValidQuery(username)) {
|
||||||
return <Navigate to="/" />;
|
return <Navigate to="/" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isValidQuery(resource) && !isValidQuery(groupErr)) {
|
||||||
|
return (
|
||||||
|
<UnauthorizedLayout>
|
||||||
|
<Trans
|
||||||
|
i18nKey="unauthorizedResourceSubtitle"
|
||||||
|
t={t}
|
||||||
|
components={{ Code: <Code /> }}
|
||||||
|
values={{ resource, username }}
|
||||||
|
/>
|
||||||
|
</UnauthorizedLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValidQuery(groupErr) && isValidQuery(resource)) {
|
||||||
|
return (
|
||||||
|
<UnauthorizedLayout>
|
||||||
|
<Trans
|
||||||
|
i18nKey="unauthorizedGroupsSubtitle"
|
||||||
|
t={t}
|
||||||
|
components={{ Code: <Code /> }}
|
||||||
|
values={{ username, resource }}
|
||||||
|
/>
|
||||||
|
</UnauthorizedLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UnauthorizedLayout>
|
||||||
|
<Trans
|
||||||
|
i18nKey="unauthorizedLoginSubtitle"
|
||||||
|
t={t}
|
||||||
|
components={{ Code: <Code /> }}
|
||||||
|
values={{ username }}
|
||||||
|
/>
|
||||||
|
</UnauthorizedLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UnauthorizedLayout = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<Paper shadow="md" p={30} mt={30} radius="md" withBorder>
|
<Paper shadow="md" p={30} mt={30} radius="md" withBorder>
|
||||||
<Text size="xl" fw={700}>
|
<Text size="xl" fw={700}>
|
||||||
{t("Unauthorized")}
|
{t("Unauthorized")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text>
|
<Text>{children}</Text>
|
||||||
{isQueryValid(resource) ? (
|
|
||||||
<Text>
|
|
||||||
<Trans
|
|
||||||
i18nKey="unauthorizedResourceSubtitle"
|
|
||||||
t={t}
|
|
||||||
components={{ Code: <Code /> }}
|
|
||||||
values={{ resource, username }}
|
|
||||||
/>
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<Text>
|
|
||||||
<Trans
|
|
||||||
i18nKey="unauthorizedLoginSubtitle"
|
|
||||||
t={t}
|
|
||||||
values={{ username }}
|
|
||||||
/>
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
mt="xl"
|
mt="xl"
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ export const appContextSchema = z.object({
|
|||||||
disableContinue: z.boolean(),
|
disableContinue: z.boolean(),
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
genericName: z.string(),
|
genericName: z.string(),
|
||||||
|
domain: z.string(),
|
||||||
|
forgotPasswordMessage: z.string(),
|
||||||
|
oauthAutoRedirect: z.enum(["none", "github", "google", "generic"]),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AppContextSchemaType = z.infer<typeof appContextSchema>;
|
export type AppContextSchemaType = z.infer<typeof appContextSchema>;
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { z } from "zod";
|
|||||||
export const userContextSchema = z.object({
|
export const userContextSchema = z.object({
|
||||||
isLoggedIn: z.boolean(),
|
isLoggedIn: z.boolean(),
|
||||||
username: z.string(),
|
username: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
email: z.string(),
|
||||||
oauth: z.boolean(),
|
oauth: z.boolean(),
|
||||||
provider: z.string(),
|
provider: z.string(),
|
||||||
totpPending: z.boolean(),
|
totpPending: z.boolean(),
|
||||||
|
|||||||
@@ -1,2 +1,17 @@
|
|||||||
export const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
|
export const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
|
||||||
export const isQueryValid = (value: string) => value.trim() !== "" && value !== "null";
|
export const escapeRegex = (value: string) => value.replace(/[-\/\\^$.*+?()[\]{}|]/g, "\\$&");
|
||||||
|
export const isValidQuery = (query: string) => query && query.trim() !== "";
|
||||||
|
|
||||||
|
export const isValidRedirectUri = (value: string) => {
|
||||||
|
if (!isValidQuery(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
new URL(value);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
70
go.mod
70
go.mod
@@ -3,25 +3,29 @@ module tinyauth
|
|||||||
go 1.23.2
|
go 1.23.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-contrib/sessions v1.0.2
|
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.10.0
|
||||||
github.com/go-playground/validator/v10 v10.24.0
|
github.com/go-playground/validator/v10 v10.26.0
|
||||||
github.com/google/go-querystring v1.1.0
|
github.com/google/go-querystring v1.1.0
|
||||||
github.com/mdp/qrterminal/v3 v3.2.0
|
github.com/mdp/qrterminal/v3 v3.2.1
|
||||||
github.com/rs/zerolog v1.33.0
|
github.com/rs/zerolog v1.34.0
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.9.1
|
||||||
github.com/spf13/viper v1.19.0
|
github.com/spf13/viper v1.20.1
|
||||||
golang.org/x/crypto v0.32.0
|
golang.org/x/crypto v0.37.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||||
|
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||||
github.com/moby/term v0.5.2 // indirect
|
github.com/moby/term v0.5.2 // indirect
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
|
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
|
||||||
golang.org/x/term v0.28.0 // indirect
|
golang.org/x/term v0.31.0 // indirect
|
||||||
gotest.tools/v3 v3.5.2 // indirect
|
gotest.tools/v3 v3.5.2 // indirect
|
||||||
rsc.io/qr v0.2.0 // indirect
|
rsc.io/qr v0.2.0 // indirect
|
||||||
)
|
)
|
||||||
@@ -33,23 +37,23 @@ require (
|
|||||||
github.com/boombuler/barcode v1.0.2 // indirect
|
github.com/boombuler/barcode v1.0.2 // indirect
|
||||||
github.com/bytedance/sonic v1.12.7 // indirect
|
github.com/bytedance/sonic v1.12.7 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.3 // indirect
|
github.com/bytedance/sonic/loader v0.2.3 // indirect
|
||||||
github.com/catppuccin/go v0.2.0 // indirect
|
github.com/catppuccin/go v0.3.0 // indirect
|
||||||
github.com/charmbracelet/bubbles v0.20.0 // indirect
|
github.com/charmbracelet/bubbles v0.21.0 // indirect
|
||||||
github.com/charmbracelet/bubbletea v1.1.0 // indirect
|
github.com/charmbracelet/bubbletea v1.3.4 // indirect
|
||||||
github.com/charmbracelet/huh v0.6.0
|
github.com/charmbracelet/huh v0.7.0
|
||||||
github.com/charmbracelet/lipgloss v0.13.0 // indirect
|
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.2.3 // indirect
|
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.0 // indirect
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/docker/docker v27.5.1+incompatible
|
github.com/docker/docker v28.1.1+incompatible
|
||||||
github.com/docker/go-connections v0.5.0 // indirect
|
github.com/docker/go-connections v0.5.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
@@ -58,57 +62,51 @@ require (
|
|||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.4 // indirect
|
github.com/goccy/go-json v0.10.4 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/gorilla/context v1.1.2 // indirect
|
|
||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
github.com/gorilla/sessions v1.2.2 // indirect
|
github.com/gorilla/sessions v1.4.0
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
github.com/magiconair/properties v1.8.7
|
github.com/magiconair/properties v1.8.10
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pquerna/otp v1.4.0
|
github.com/pquerna/otp v1.4.0
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
|
||||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
github.com/spf13/afero v1.11.0 // indirect
|
github.com/spf13/afero v1.12.0 // indirect
|
||||||
github.com/spf13/cast v1.6.0 // indirect
|
github.com/spf13/cast v1.7.1 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.6 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.34.0 // indirect
|
go.opentelemetry.io/otel v1.34.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.34.0 // indirect
|
go.opentelemetry.io/otel/metric v1.34.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||||
go.uber.org/atomic v1.9.0 // indirect
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
go.uber.org/multierr v1.9.0 // indirect
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
golang.org/x/arch v0.13.0 // indirect
|
golang.org/x/arch v0.13.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
golang.org/x/net v0.38.0 // indirect
|
||||||
golang.org/x/net v0.34.0 // indirect
|
golang.org/x/oauth2 v0.29.0
|
||||||
golang.org/x/oauth2 v0.25.0
|
golang.org/x/sync v0.13.0 // indirect
|
||||||
golang.org/x/sync v0.10.0 // indirect
|
golang.org/x/sys v0.32.0 // indirect
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
golang.org/x/text v0.24.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.36.3 // indirect
|
google.golang.org/protobuf v1.36.3 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
174
go.sum
174
go.sum
@@ -8,6 +8,8 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
|
|||||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
||||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
|
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
|
||||||
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
@@ -16,39 +18,54 @@ github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOE
|
|||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
|
github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
|
||||||
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
|
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
||||||
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
|
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
||||||
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
|
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||||
github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c=
|
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
|
||||||
github.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4=
|
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
|
||||||
github.com/charmbracelet/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8=
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||||
github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU=
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||||
github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
|
github.com/charmbracelet/huh v0.7.0 h1:W8S1uyGETgj9Tuda3/JdVkc3x7DBLZYPZc4c+/rnRdc=
|
||||||
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
|
github.com/charmbracelet/huh v0.7.0/go.mod h1:UGC3DZHlgOKHvHC07a5vHag41zzhpPFj34U92sOmyuk=
|
||||||
github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY=
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
|
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||||
|
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||||
|
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
|
||||||
|
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
|
||||||
|
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
|
||||||
|
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
|
||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||||
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
|
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||||
github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
|
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||||
|
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||||
|
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
|
||||||
|
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
|
||||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||||
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/docker/docker v27.5.1+incompatible h1:4PYU5dnBYqRQi0294d1FBECqT9ECWeQAIfE8q4YnPY8=
|
github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I=
|
||||||
github.com/docker/docker v27.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
@@ -61,12 +78,10 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
|||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
github.com/gin-contrib/sessions v1.0.2 h1:UaIjUvTH1cMeOdj3in6dl+Xb6It8RiKRF9Z1anbUyCA=
|
|
||||||
github.com/gin-contrib/sessions v1.0.2/go.mod h1:KxKxWqWP5LJVDCInulOl4WbLzK2KSPlLesfZ66wRvMs=
|
|
||||||
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
||||||
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
@@ -82,8 +97,10 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
|||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
|
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||||
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
||||||
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
@@ -99,16 +116,12 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
|||||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
|
||||||
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
|
||||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||||
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
|
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
||||||
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
|
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
@@ -128,8 +141,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
|||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
@@ -141,14 +154,16 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J
|
|||||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mdp/qrterminal/v3 v3.2.0 h1:qteQMXO3oyTK4IHwj2mWsKYYRBOp1Pj2WRYFYYNTCdk=
|
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
|
||||||
github.com/mdp/qrterminal/v3 v3.2.0/go.mod h1:XGGuua4Lefrl7TLEsSONiD+UEjQXJZ4mPzF+gWYIJkk=
|
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU=
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
|
||||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
|
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||||
|
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||||
|
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||||
|
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@@ -162,8 +177,8 @@ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D
|
|||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg=
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||||
@@ -173,9 +188,8 @@ github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xl
|
|||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
|
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
|
||||||
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
@@ -183,29 +197,27 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
||||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
|
||||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
||||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
@@ -226,12 +238,14 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
|
||||||
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||||
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
|
||||||
@@ -255,25 +269,25 @@ golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
|||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
|
||||||
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -283,16 +297,16 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
@@ -301,7 +315,7 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
|||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
|
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
|
||||||
@@ -313,8 +327,6 @@ google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojt
|
|||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
|
||||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ import (
|
|||||||
"tinyauth/internal/handlers"
|
"tinyauth/internal/handlers"
|
||||||
"tinyauth/internal/types"
|
"tinyauth/internal/types"
|
||||||
|
|
||||||
"github.com/gin-contrib/sessions"
|
|
||||||
"github.com/gin-contrib/sessions/cookie"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
@@ -51,21 +49,6 @@ func (api *API) Init() {
|
|||||||
log.Debug().Msg("Setting up file server")
|
log.Debug().Msg("Setting up file server")
|
||||||
fileServer := http.FileServer(http.FS(dist))
|
fileServer := http.FileServer(http.FS(dist))
|
||||||
|
|
||||||
// Setup cookie store
|
|
||||||
log.Debug().Msg("Setting up cookie store")
|
|
||||||
store := cookie.NewStore([]byte(api.Config.Secret))
|
|
||||||
|
|
||||||
// Use session middleware
|
|
||||||
store.Options(sessions.Options{
|
|
||||||
Domain: api.Config.Domain,
|
|
||||||
Path: "/",
|
|
||||||
HttpOnly: true,
|
|
||||||
Secure: api.Config.CookieSecure,
|
|
||||||
MaxAge: api.Config.SessionExpiry,
|
|
||||||
})
|
|
||||||
|
|
||||||
router.Use(sessions.Sessions("tinyauth", store))
|
|
||||||
|
|
||||||
// UI middleware
|
// UI middleware
|
||||||
router.Use(func(c *gin.Context) {
|
router.Use(func(c *gin.Context) {
|
||||||
// If not an API request, serve the UI
|
// If not an API request, serve the UI
|
||||||
|
|||||||
@@ -21,21 +21,33 @@ import (
|
|||||||
|
|
||||||
// Simple API config for tests
|
// Simple API config for tests
|
||||||
var apiConfig = types.APIConfig{
|
var apiConfig = types.APIConfig{
|
||||||
Port: 8080,
|
Port: 8080,
|
||||||
Address: "0.0.0.0",
|
Address: "0.0.0.0",
|
||||||
Secret: "super-secret-api-thing-for-tests", // It is 32 chars long
|
|
||||||
CookieSecure: false,
|
|
||||||
SessionExpiry: 3600,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple handlers config for tests
|
// Simple handlers config for tests
|
||||||
var handlersConfig = types.HandlersConfig{
|
var handlersConfig = types.HandlersConfig{
|
||||||
AppURL: "http://localhost:8080",
|
AppURL: "http://localhost:8080",
|
||||||
Domain: ".localhost",
|
DisableContinue: false,
|
||||||
|
Title: "Tinyauth",
|
||||||
|
GenericName: "Generic",
|
||||||
|
ForgotPasswordMessage: "Some message",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple auth config for tests
|
||||||
|
var authConfig = types.AuthConfig{
|
||||||
|
Users: types.Users{},
|
||||||
|
OauthWhitelist: "",
|
||||||
|
Secret: "super-secret-api-thing-for-tests", // It is 32 chars long
|
||||||
CookieSecure: false,
|
CookieSecure: false,
|
||||||
DisableContinue: false,
|
SessionExpiry: 3600,
|
||||||
Title: "Tinyauth",
|
LoginTimeout: 0,
|
||||||
GenericName: "Generic",
|
LoginMaxRetries: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple hooks config for tests
|
||||||
|
var hooksConfig = types.HooksConfig{
|
||||||
|
Domain: "localhost",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cookie
|
// Cookie
|
||||||
@@ -61,12 +73,13 @@ func getAPI(t *testing.T) *api.API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create auth service
|
// Create auth service
|
||||||
auth := auth.NewAuth(docker, types.Users{
|
authConfig.Users = types.Users{
|
||||||
{
|
{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Password: user.Password,
|
Password: user.Password,
|
||||||
},
|
},
|
||||||
}, nil, apiConfig.SessionExpiry)
|
}
|
||||||
|
auth := auth.NewAuth(authConfig, docker)
|
||||||
|
|
||||||
// Create providers service
|
// Create providers service
|
||||||
providers := providers.NewProviders(types.OAuthConfig{})
|
providers := providers.NewProviders(types.OAuthConfig{})
|
||||||
@@ -75,7 +88,7 @@ func getAPI(t *testing.T) *api.API {
|
|||||||
providers.Init()
|
providers.Init()
|
||||||
|
|
||||||
// Create hooks service
|
// Create hooks service
|
||||||
hooks := hooks.NewHooks(auth, providers)
|
hooks := hooks.NewHooks(hooksConfig, auth, providers)
|
||||||
|
|
||||||
// Create handlers service
|
// Create handlers service
|
||||||
handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker)
|
handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker)
|
||||||
@@ -186,12 +199,13 @@ func TestAppContext(t *testing.T) {
|
|||||||
|
|
||||||
// Create tests values
|
// Create tests values
|
||||||
expected := types.AppContext{
|
expected := types.AppContext{
|
||||||
Status: 200,
|
Status: 200,
|
||||||
Message: "OK",
|
Message: "OK",
|
||||||
ConfiguredProviders: []string{"username"},
|
ConfiguredProviders: []string{"username"},
|
||||||
DisableContinue: false,
|
DisableContinue: false,
|
||||||
Title: "Tinyauth",
|
Title: "Tinyauth",
|
||||||
GenericName: "Generic",
|
GenericName: "Generic",
|
||||||
|
ForgotPasswordMessage: "Some message",
|
||||||
}
|
}
|
||||||
|
|
||||||
// We should get the username back
|
// We should get the username back
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
v3.2.0
|
v3.3.0
|
||||||
@@ -1,38 +1,62 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"tinyauth/internal/docker"
|
"tinyauth/internal/docker"
|
||||||
"tinyauth/internal/types"
|
"tinyauth/internal/types"
|
||||||
|
"tinyauth/internal/utils"
|
||||||
|
|
||||||
"github.com/gin-contrib/sessions"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewAuth(docker *docker.Docker, userList types.Users, oauthWhitelist []string, sessionExpiry int) *Auth {
|
func NewAuth(config types.AuthConfig, docker *docker.Docker) *Auth {
|
||||||
return &Auth{
|
return &Auth{
|
||||||
Docker: docker,
|
Config: config,
|
||||||
Users: userList,
|
Docker: docker,
|
||||||
OAuthWhitelist: oauthWhitelist,
|
LoginAttempts: make(map[string]*types.LoginAttempt),
|
||||||
SessionExpiry: sessionExpiry,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Auth struct {
|
type Auth struct {
|
||||||
Users types.Users
|
Config types.AuthConfig
|
||||||
Docker *docker.Docker
|
Docker *docker.Docker
|
||||||
OAuthWhitelist []string
|
LoginAttempts map[string]*types.LoginAttempt
|
||||||
SessionExpiry int
|
LoginMutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *Auth) GetSession(c *gin.Context) (*sessions.Session, error) {
|
||||||
|
// Create cookie store
|
||||||
|
store := sessions.NewCookieStore([]byte(auth.Config.Secret))
|
||||||
|
|
||||||
|
// Configure cookie store
|
||||||
|
store.Options = &sessions.Options{
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: auth.Config.SessionExpiry,
|
||||||
|
Secure: auth.Config.CookieSecure,
|
||||||
|
HttpOnly: true,
|
||||||
|
Domain: fmt.Sprintf(".%s", auth.Config.Domain),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get session
|
||||||
|
session, err := store.Get(c.Request, "tinyauth")
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to get session")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return session, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *Auth) GetUser(username string) *types.User {
|
func (auth *Auth) GetUser(username string) *types.User {
|
||||||
// Loop through users and return the user if the username matches
|
// Loop through users and return the user if the username matches
|
||||||
for _, user := range auth.Users {
|
for _, user := range auth.Config.Users {
|
||||||
if user.Username == username {
|
if user.Username == username {
|
||||||
return &user
|
return &user
|
||||||
}
|
}
|
||||||
@@ -45,28 +69,83 @@ func (auth *Auth) CheckPassword(user types.User, password string) bool {
|
|||||||
return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil
|
return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *Auth) EmailWhitelisted(emailSrc string) bool {
|
// IsAccountLocked checks if a username or IP is locked due to too many failed login attempts
|
||||||
// If the whitelist is empty, allow all emails
|
func (auth *Auth) IsAccountLocked(identifier string) (bool, int) {
|
||||||
if len(auth.OAuthWhitelist) == 0 {
|
auth.LoginMutex.RLock()
|
||||||
return true
|
defer auth.LoginMutex.RUnlock()
|
||||||
|
|
||||||
|
// Return false if rate limiting is not configured
|
||||||
|
if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 {
|
||||||
|
return false, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop through the whitelist and return true if the email matches
|
// Check if the identifier exists in the map
|
||||||
for _, email := range auth.OAuthWhitelist {
|
attempt, exists := auth.LoginAttempts[identifier]
|
||||||
if email == emailSrc {
|
if !exists {
|
||||||
return true
|
return false, 0
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no emails match, return false
|
// If account is locked, check if lock time has expired
|
||||||
return false
|
if attempt.LockedUntil.After(time.Now()) {
|
||||||
|
// Calculate remaining lockout time in seconds
|
||||||
|
remaining := int(time.Until(attempt.LockedUntil).Seconds())
|
||||||
|
return true, remaining
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock has expired
|
||||||
|
return false, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) {
|
// RecordLoginAttempt records a login attempt for rate limiting
|
||||||
|
func (auth *Auth) RecordLoginAttempt(identifier string, success bool) {
|
||||||
|
// Skip if rate limiting is not configured
|
||||||
|
if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.LoginMutex.Lock()
|
||||||
|
defer auth.LoginMutex.Unlock()
|
||||||
|
|
||||||
|
// Get current attempt record or create a new one
|
||||||
|
attempt, exists := auth.LoginAttempts[identifier]
|
||||||
|
if !exists {
|
||||||
|
attempt = &types.LoginAttempt{}
|
||||||
|
auth.LoginAttempts[identifier] = attempt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last attempt time
|
||||||
|
attempt.LastAttempt = time.Now()
|
||||||
|
|
||||||
|
// If successful login, reset failed attempts
|
||||||
|
if success {
|
||||||
|
attempt.FailedAttempts = 0
|
||||||
|
attempt.LockedUntil = time.Time{} // Reset lock time
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment failed attempts
|
||||||
|
attempt.FailedAttempts++
|
||||||
|
|
||||||
|
// If max retries reached, lock the account
|
||||||
|
if attempt.FailedAttempts >= auth.Config.LoginMaxRetries {
|
||||||
|
attempt.LockedUntil = time.Now().Add(time.Duration(auth.Config.LoginTimeout) * time.Second)
|
||||||
|
log.Warn().Str("identifier", identifier).Int("timeout", auth.Config.LoginTimeout).Msg("Account locked due to too many failed login attempts")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *Auth) EmailWhitelisted(emailSrc string) bool {
|
||||||
|
return utils.CheckWhitelist(auth.Config.OauthWhitelist, emailSrc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) error {
|
||||||
log.Debug().Msg("Creating session cookie")
|
log.Debug().Msg("Creating session cookie")
|
||||||
|
|
||||||
// Get session
|
// Get session
|
||||||
sessions := sessions.Default(c)
|
session, err := auth.GetSession(c)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to get session")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Setting session cookie")
|
log.Debug().Msg("Setting session cookie")
|
||||||
|
|
||||||
@@ -76,54 +155,84 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie)
|
|||||||
if data.TotpPending {
|
if data.TotpPending {
|
||||||
sessionExpiry = 3600
|
sessionExpiry = 3600
|
||||||
} else {
|
} else {
|
||||||
sessionExpiry = auth.SessionExpiry
|
sessionExpiry = auth.Config.SessionExpiry
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set data
|
// Set data
|
||||||
sessions.Set("username", data.Username)
|
session.Values["username"] = data.Username
|
||||||
sessions.Set("provider", data.Provider)
|
session.Values["name"] = data.Name
|
||||||
sessions.Set("expiry", time.Now().Add(time.Duration(sessionExpiry)*time.Second).Unix())
|
session.Values["email"] = data.Email
|
||||||
sessions.Set("totpPending", data.TotpPending)
|
session.Values["provider"] = data.Provider
|
||||||
|
session.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix()
|
||||||
|
session.Values["totpPending"] = data.TotpPending
|
||||||
|
session.Values["oauthGroups"] = data.OAuthGroups
|
||||||
|
|
||||||
// Save session
|
// Save session
|
||||||
sessions.Save()
|
err = session.Save(c.Request, c.Writer)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to save session")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return nil
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *Auth) DeleteSessionCookie(c *gin.Context) {
|
func (auth *Auth) DeleteSessionCookie(c *gin.Context) error {
|
||||||
log.Debug().Msg("Deleting session cookie")
|
log.Debug().Msg("Deleting session cookie")
|
||||||
|
|
||||||
// Get session
|
// Get session
|
||||||
sessions := sessions.Default(c)
|
session, err := auth.GetSession(c)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to get session")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Clear session
|
// Delete all values in the session
|
||||||
sessions.Clear()
|
for key := range session.Values {
|
||||||
|
delete(session.Values, key)
|
||||||
|
}
|
||||||
|
|
||||||
// Save session
|
// Save session
|
||||||
sessions.Save()
|
err = session.Save(c.Request, c.Writer)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to save session")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return nil
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *Auth) GetSessionCookie(c *gin.Context) types.SessionCookie {
|
func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) {
|
||||||
log.Debug().Msg("Getting session cookie")
|
log.Debug().Msg("Getting session cookie")
|
||||||
|
|
||||||
// Get session
|
// Get session
|
||||||
sessions := sessions.Default(c)
|
session, err := auth.GetSession(c)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to get session")
|
||||||
|
return types.SessionCookie{}, err
|
||||||
|
}
|
||||||
|
|
||||||
// Get data
|
log.Debug().Msg("Got session")
|
||||||
cookieUsername := sessions.Get("username")
|
|
||||||
cookieProvider := sessions.Get("provider")
|
|
||||||
cookieExpiry := sessions.Get("expiry")
|
|
||||||
cookieTotpPending := sessions.Get("totpPending")
|
|
||||||
|
|
||||||
// Convert interfaces to correct types
|
// Get data from session
|
||||||
username, usernameOk := cookieUsername.(string)
|
username, usernameOk := session.Values["username"].(string)
|
||||||
provider, providerOk := cookieProvider.(string)
|
email, emailOk := session.Values["email"].(string)
|
||||||
expiry, expiryOk := cookieExpiry.(int64)
|
name, nameOk := session.Values["name"].(string)
|
||||||
totpPending, totpPendingOk := cookieTotpPending.(bool)
|
provider, providerOK := session.Values["provider"].(string)
|
||||||
|
expiry, expiryOk := session.Values["expiry"].(int64)
|
||||||
|
totpPending, totpPendingOk := session.Values["totpPending"].(bool)
|
||||||
|
oauthGroups, oauthGroupsOk := session.Values["oauthGroups"].(string)
|
||||||
|
|
||||||
// Check if the cookie is invalid
|
if !usernameOk || !providerOK || !expiryOk || !totpPendingOk || !emailOk || !nameOk || !oauthGroupsOk {
|
||||||
if !usernameOk || !providerOk || !expiryOk || !totpPendingOk {
|
log.Warn().Msg("Session cookie is invalid")
|
||||||
log.Warn().Msg("Session cookie invalid")
|
|
||||||
return types.SessionCookie{}
|
// If any data is missing, delete the session cookie
|
||||||
|
auth.DeleteSessionCookie(c)
|
||||||
|
|
||||||
|
// Return empty cookie
|
||||||
|
return types.SessionCookie{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the cookie has expired
|
// Check if the cookie has expired
|
||||||
@@ -134,77 +243,73 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) types.SessionCookie {
|
|||||||
auth.DeleteSessionCookie(c)
|
auth.DeleteSessionCookie(c)
|
||||||
|
|
||||||
// Return empty cookie
|
// Return empty cookie
|
||||||
return types.SessionCookie{}
|
return types.SessionCookie{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Str("username", username).Str("provider", provider).Int64("expiry", expiry).Bool("totpPending", totpPending).Msg("Parsed cookie")
|
log.Debug().Str("username", username).Str("provider", provider).Int64("expiry", expiry).Bool("totpPending", totpPending).Str("name", name).Str("email", email).Str("oauthGroups", oauthGroups).Msg("Parsed cookie")
|
||||||
|
|
||||||
// Return the cookie
|
// Return the cookie
|
||||||
return types.SessionCookie{
|
return types.SessionCookie{
|
||||||
Username: username,
|
Username: username,
|
||||||
|
Name: name,
|
||||||
|
Email: email,
|
||||||
Provider: provider,
|
Provider: provider,
|
||||||
TotpPending: totpPending,
|
TotpPending: totpPending,
|
||||||
}
|
OAuthGroups: oauthGroups,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *Auth) UserAuthConfigured() bool {
|
func (auth *Auth) UserAuthConfigured() bool {
|
||||||
// If there are users, return true
|
// If there are users, return true
|
||||||
return len(auth.Users) > 0
|
return len(auth.Config.Users) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext) (bool, error) {
|
func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext, labels types.TinyauthLabels) bool {
|
||||||
// Get headers
|
|
||||||
host := c.Request.Header.Get("X-Forwarded-Host")
|
|
||||||
|
|
||||||
// Get app id
|
|
||||||
appId := strings.Split(host, ".")[0]
|
|
||||||
|
|
||||||
// Get the container labels
|
|
||||||
labels, err := auth.Docker.GetLabels(appId)
|
|
||||||
|
|
||||||
// If there is an error, return false
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if oauth is allowed
|
// Check if oauth is allowed
|
||||||
if context.OAuth {
|
if context.OAuth {
|
||||||
if len(labels.OAuthWhitelist) == 0 {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
log.Debug().Msg("Checking OAuth whitelist")
|
log.Debug().Msg("Checking OAuth whitelist")
|
||||||
if slices.Contains(labels.OAuthWhitelist, context.Username) {
|
return utils.CheckWhitelist(labels.OAuthWhitelist, context.Email)
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user is allowed
|
// Check users
|
||||||
if len(labels.Users) != 0 {
|
log.Debug().Msg("Checking users")
|
||||||
log.Debug().Msg("Checking users")
|
|
||||||
if slices.Contains(labels.Users, context.Username) {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not allowed
|
return utils.CheckWhitelist(labels.Users, context.Username)
|
||||||
return false, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *Auth) AuthEnabled(c *gin.Context) (bool, error) {
|
func (auth *Auth) OAuthGroup(c *gin.Context, context types.UserContext, labels types.TinyauthLabels) bool {
|
||||||
|
// Check if groups are required
|
||||||
|
if labels.OAuthGroups == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we are using the generic oauth provider
|
||||||
|
if context.Provider != "generic" {
|
||||||
|
log.Debug().Msg("Not using generic provider, skipping group check")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split the groups by comma (no need to parse since they are from the API response)
|
||||||
|
oauthGroups := strings.Split(context.OAuthGroups, ",")
|
||||||
|
|
||||||
|
// For every group check if it is in the required groups
|
||||||
|
for _, group := range oauthGroups {
|
||||||
|
if utils.CheckWhitelist(labels.OAuthGroups, group) {
|
||||||
|
log.Debug().Str("group", group).Msg("Group is in required groups")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No groups matched
|
||||||
|
log.Debug().Msg("No groups matched")
|
||||||
|
|
||||||
|
// Return false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *Auth) AuthEnabled(c *gin.Context, labels types.TinyauthLabels) (bool, error) {
|
||||||
// Get headers
|
// Get headers
|
||||||
uri := c.Request.Header.Get("X-Forwarded-Uri")
|
uri := c.Request.Header.Get("X-Forwarded-Uri")
|
||||||
host := c.Request.Header.Get("X-Forwarded-Host")
|
|
||||||
|
|
||||||
// Get app id
|
|
||||||
appId := strings.Split(host, ".")[0]
|
|
||||||
|
|
||||||
// Get the container labels
|
|
||||||
labels, err := auth.Docker.GetLabels(appId)
|
|
||||||
|
|
||||||
// If there is an error, auth enabled
|
|
||||||
if err != nil {
|
|
||||||
return true, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the allowed label is empty
|
// Check if the allowed label is empty
|
||||||
if labels.Allowed == "" {
|
if labels.Allowed == "" {
|
||||||
|
|||||||
147
internal/auth/auth_test.go
Normal file
147
internal/auth/auth_test.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
package auth_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
"tinyauth/internal/auth"
|
||||||
|
"tinyauth/internal/docker"
|
||||||
|
"tinyauth/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var config = types.AuthConfig{
|
||||||
|
Users: types.Users{},
|
||||||
|
OauthWhitelist: "",
|
||||||
|
SessionExpiry: 3600,
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoginRateLimiting(t *testing.T) {
|
||||||
|
// Initialize a new auth service with 3 max retries and 5 seconds timeout
|
||||||
|
config.LoginMaxRetries = 3
|
||||||
|
config.LoginTimeout = 5
|
||||||
|
authService := auth.NewAuth(config, &docker.Docker{})
|
||||||
|
|
||||||
|
// Test identifier
|
||||||
|
identifier := "test_user"
|
||||||
|
|
||||||
|
// Test successful login - should not lock account
|
||||||
|
t.Log("Testing successful login")
|
||||||
|
|
||||||
|
authService.RecordLoginAttempt(identifier, true)
|
||||||
|
locked, _ := authService.IsAccountLocked(identifier)
|
||||||
|
|
||||||
|
if locked {
|
||||||
|
t.Fatalf("Account should not be locked after successful login")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 2 failed attempts - should not lock account yet
|
||||||
|
t.Log("Testing 2 failed login attempts")
|
||||||
|
|
||||||
|
authService.RecordLoginAttempt(identifier, false)
|
||||||
|
authService.RecordLoginAttempt(identifier, false)
|
||||||
|
locked, _ = authService.IsAccountLocked(identifier)
|
||||||
|
|
||||||
|
if locked {
|
||||||
|
t.Fatalf("Account should not be locked after only 2 failed attempts")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add one more failed attempt (total 3) - should lock account with maxRetries=3
|
||||||
|
t.Log("Testing 3 failed login attempts")
|
||||||
|
authService.RecordLoginAttempt(identifier, false)
|
||||||
|
locked, remainingTime := authService.IsAccountLocked(identifier)
|
||||||
|
|
||||||
|
if !locked {
|
||||||
|
t.Fatalf("Account should be locked after reaching max retries")
|
||||||
|
}
|
||||||
|
if remainingTime <= 0 || remainingTime > 5 {
|
||||||
|
t.Fatalf("Expected remaining time between 1-5 seconds, got %d", remainingTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test reset after waiting for timeout - use 1 second timeout for fast testing
|
||||||
|
t.Log("Testing unlocking after timeout")
|
||||||
|
|
||||||
|
// Reinitialize auth service with a shorter timeout for testing
|
||||||
|
config.LoginTimeout = 1
|
||||||
|
config.LoginMaxRetries = 3
|
||||||
|
authService = auth.NewAuth(config, &docker.Docker{})
|
||||||
|
|
||||||
|
// Add enough failed attempts to lock the account
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
authService.RecordLoginAttempt(identifier, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it's locked
|
||||||
|
locked, _ = authService.IsAccountLocked(identifier)
|
||||||
|
if !locked {
|
||||||
|
t.Fatalf("Account should be locked initially")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait a bit and verify it gets unlocked after timeout
|
||||||
|
time.Sleep(1500 * time.Millisecond) // Wait longer than the timeout
|
||||||
|
locked, _ = authService.IsAccountLocked(identifier)
|
||||||
|
|
||||||
|
if locked {
|
||||||
|
t.Fatalf("Account should be unlocked after timeout period")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test disabled rate limiting
|
||||||
|
t.Log("Testing disabled rate limiting")
|
||||||
|
config.LoginMaxRetries = 0
|
||||||
|
config.LoginTimeout = 0
|
||||||
|
authService = auth.NewAuth(config, &docker.Docker{})
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
authService.RecordLoginAttempt(identifier, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
locked, _ = authService.IsAccountLocked(identifier)
|
||||||
|
if locked {
|
||||||
|
t.Fatalf("Account should not be locked when rate limiting is disabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConcurrentLoginAttempts(t *testing.T) {
|
||||||
|
// Initialize a new auth service with 2 max retries and 5 seconds timeout
|
||||||
|
config.LoginMaxRetries = 2
|
||||||
|
config.LoginTimeout = 5
|
||||||
|
authService := auth.NewAuth(config, &docker.Docker{})
|
||||||
|
|
||||||
|
// Test multiple identifiers
|
||||||
|
identifiers := []string{"user1", "user2", "user3"}
|
||||||
|
|
||||||
|
// Test that locking one identifier doesn't affect others
|
||||||
|
t.Log("Testing multiple identifiers")
|
||||||
|
|
||||||
|
// Add enough failed attempts to lock first user (2 attempts with maxRetries=2)
|
||||||
|
authService.RecordLoginAttempt(identifiers[0], false)
|
||||||
|
authService.RecordLoginAttempt(identifiers[0], false)
|
||||||
|
|
||||||
|
// Check if first user is locked
|
||||||
|
locked, _ := authService.IsAccountLocked(identifiers[0])
|
||||||
|
if !locked {
|
||||||
|
t.Fatalf("User1 should be locked after reaching max retries")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that other users are not affected
|
||||||
|
for i := 1; i < len(identifiers); i++ {
|
||||||
|
locked, _ := authService.IsAccountLocked(identifiers[i])
|
||||||
|
if locked {
|
||||||
|
t.Fatalf("User%d should not be locked", i+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test successful login after failed attempts (but before lock)
|
||||||
|
t.Log("Testing successful login after failed attempts but before lock")
|
||||||
|
|
||||||
|
// One failed attempt for user2
|
||||||
|
authService.RecordLoginAttempt(identifiers[1], false)
|
||||||
|
|
||||||
|
// Successful login should reset the counter
|
||||||
|
authService.RecordLoginAttempt(identifiers[1], true)
|
||||||
|
|
||||||
|
// Now try a failed login again - should not be locked as counter was reset
|
||||||
|
authService.RecordLoginAttempt(identifiers[1], false)
|
||||||
|
locked, _ = authService.IsAccountLocked(identifiers[1])
|
||||||
|
if locked {
|
||||||
|
t.Fatalf("User2 should not be locked after successful login reset")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,4 +6,13 @@ var TinyauthLabels = []string{
|
|||||||
"tinyauth.users",
|
"tinyauth.users",
|
||||||
"tinyauth.allowed",
|
"tinyauth.allowed",
|
||||||
"tinyauth.headers",
|
"tinyauth.headers",
|
||||||
|
"tinyauth.oauth.groups",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Claims are the OIDC supported claims (including preferd username for some reason)
|
||||||
|
type Claims struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
PreferredUsername string `json:"preferred_username"`
|
||||||
|
Groups []string `json:"groups"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import (
|
|||||||
"tinyauth/internal/types"
|
"tinyauth/internal/types"
|
||||||
"tinyauth/internal/utils"
|
"tinyauth/internal/utils"
|
||||||
|
|
||||||
apiTypes "github.com/docker/docker/api/types"
|
container "github.com/docker/docker/api/types/container"
|
||||||
containerTypes "github.com/docker/docker/api/types/container"
|
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
@@ -38,9 +37,9 @@ func (docker *Docker) Init() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (docker *Docker) GetContainers() ([]apiTypes.Container, error) {
|
func (docker *Docker) GetContainers() ([]container.Summary, error) {
|
||||||
// Get the list of containers
|
// Get the list of containers
|
||||||
containers, err := docker.Client.ContainerList(docker.Context, containerTypes.ListOptions{})
|
containers, err := docker.Client.ContainerList(docker.Context, container.ListOptions{})
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -51,13 +50,13 @@ func (docker *Docker) GetContainers() ([]apiTypes.Container, error) {
|
|||||||
return containers, nil
|
return containers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (docker *Docker) InspectContainer(containerId string) (apiTypes.ContainerJSON, error) {
|
func (docker *Docker) InspectContainer(containerId string) (container.InspectResponse, error) {
|
||||||
// Inspect the container
|
// Inspect the container
|
||||||
inspect, err := docker.Client.ContainerInspect(docker.Context, containerId)
|
inspect, err := docker.Client.ContainerInspect(docker.Context, containerId)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return apiTypes.ContainerJSON{}, err
|
return container.InspectResponse{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the inspect
|
// Return the inspect
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand/v2"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
"tinyauth/internal/auth"
|
"tinyauth/internal/auth"
|
||||||
"tinyauth/internal/docker"
|
"tinyauth/internal/docker"
|
||||||
"tinyauth/internal/hooks"
|
"tinyauth/internal/hooks"
|
||||||
"tinyauth/internal/providers"
|
"tinyauth/internal/providers"
|
||||||
"tinyauth/internal/types"
|
"tinyauth/internal/types"
|
||||||
|
"tinyauth/internal/utils"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/go-querystring/query"
|
"github.com/google/go-querystring/query"
|
||||||
@@ -68,12 +69,15 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
|
|||||||
proto := c.Request.Header.Get("X-Forwarded-Proto")
|
proto := c.Request.Header.Get("X-Forwarded-Proto")
|
||||||
host := c.Request.Header.Get("X-Forwarded-Host")
|
host := c.Request.Header.Get("X-Forwarded-Host")
|
||||||
|
|
||||||
// Check if auth is enabled
|
// Get the app id
|
||||||
authEnabled, err := h.Auth.AuthEnabled(c)
|
appId := strings.Split(host, ".")[0]
|
||||||
|
|
||||||
|
// Get the container labels
|
||||||
|
labels, err := h.Docker.GetLabels(appId)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to check if app is allowed")
|
log.Error().Err(err).Msg("Failed to get container labels")
|
||||||
|
|
||||||
if proxy.Proxy == "nginx" || !isBrowser {
|
if proxy.Proxy == "nginx" || !isBrowser {
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, gin.H{
|
||||||
@@ -87,11 +91,8 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the app id
|
// Check if auth is enabled
|
||||||
appId := strings.Split(host, ".")[0]
|
authEnabled, err := h.Auth.AuthEnabled(c, labels)
|
||||||
|
|
||||||
// Get the container labels
|
|
||||||
labels, err := h.Docker.GetLabels(appId)
|
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -113,7 +114,7 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
|
|||||||
if !authEnabled {
|
if !authEnabled {
|
||||||
for key, value := range labels.Headers {
|
for key, value := range labels.Headers {
|
||||||
log.Debug().Str("key", key).Str("value", value).Msg("Setting header")
|
log.Debug().Str("key", key).Str("value", value).Msg("Setting header")
|
||||||
c.Header(key, value)
|
c.Header(key, utils.SanitizeHeader(value))
|
||||||
}
|
}
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
@@ -125,28 +126,18 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
|
|||||||
// Get user context
|
// Get user context
|
||||||
userContext := h.Hooks.UseUserContext(c)
|
userContext := h.Hooks.UseUserContext(c)
|
||||||
|
|
||||||
|
// If we are using basic auth, we need to check if the user has totp and if it does then disable basic auth
|
||||||
|
if userContext.Provider == "basic" && userContext.TotpEnabled {
|
||||||
|
log.Warn().Str("username", userContext.Username).Msg("User has totp enabled, disabling basic auth")
|
||||||
|
userContext.IsLoggedIn = false
|
||||||
|
}
|
||||||
|
|
||||||
// Check if user is logged in
|
// Check if user is logged in
|
||||||
if userContext.IsLoggedIn {
|
if userContext.IsLoggedIn {
|
||||||
log.Debug().Msg("Authenticated")
|
log.Debug().Msg("Authenticated")
|
||||||
|
|
||||||
// Check if user is allowed to access subdomain, if request is nginx.example.com the subdomain (resource) is nginx
|
// Check if user is allowed to access subdomain, if request is nginx.example.com the subdomain (resource) is nginx
|
||||||
appAllowed, err := h.Auth.ResourceAllowed(c, userContext)
|
appAllowed := h.Auth.ResourceAllowed(c, userContext, labels)
|
||||||
|
|
||||||
// Check if there was an error
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Failed to check if app is allowed")
|
|
||||||
|
|
||||||
if proxy.Proxy == "nginx" || !isBrowser {
|
|
||||||
c.JSON(500, gin.H{
|
|
||||||
"status": 500,
|
|
||||||
"message": "Internal Server Error",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Bool("appAllowed", appAllowed).Msg("Checking if app is allowed")
|
log.Debug().Bool("appAllowed", appAllowed).Msg("Checking if app is allowed")
|
||||||
|
|
||||||
@@ -165,11 +156,20 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build query
|
// Values
|
||||||
queries, err := query.Values(types.UnauthorizedQuery{
|
values := types.UnauthorizedQuery{
|
||||||
Username: userContext.Username,
|
|
||||||
Resource: strings.Split(host, ".")[0],
|
Resource: strings.Split(host, ".")[0],
|
||||||
})
|
}
|
||||||
|
|
||||||
|
// Use either username or email
|
||||||
|
if userContext.OAuth {
|
||||||
|
values.Username = userContext.Email
|
||||||
|
} else {
|
||||||
|
values.Username = userContext.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build query
|
||||||
|
queries, err := query.Values(values)
|
||||||
|
|
||||||
// Handle error (no need to check for nginx/headers since we are sure we are using caddy/traefik)
|
// Handle error (no need to check for nginx/headers since we are sure we are using caddy/traefik)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -183,13 +183,65 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the user header
|
log.Debug().Interface("labels", labels).Msg("Got labels")
|
||||||
c.Header("Remote-User", userContext.Username)
|
|
||||||
|
// Check if user is in required groups
|
||||||
|
groupOk := h.Auth.OAuthGroup(c, userContext, labels)
|
||||||
|
|
||||||
|
log.Debug().Bool("groupOk", groupOk).Msg("Checking if user is in required groups")
|
||||||
|
|
||||||
|
// The user is not allowed to access the app
|
||||||
|
if !groupOk {
|
||||||
|
log.Warn().Str("username", userContext.Username).Str("host", host).Msg("User is not in required groups")
|
||||||
|
|
||||||
|
// Set WWW-Authenticate header
|
||||||
|
c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"")
|
||||||
|
|
||||||
|
if proxy.Proxy == "nginx" || !isBrowser {
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"status": 401,
|
||||||
|
"message": "Unauthorized",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values
|
||||||
|
values := types.UnauthorizedQuery{
|
||||||
|
Resource: strings.Split(host, ".")[0],
|
||||||
|
GroupErr: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use either username or email
|
||||||
|
if userContext.OAuth {
|
||||||
|
values.Username = userContext.Email
|
||||||
|
} else {
|
||||||
|
values.Username = userContext.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build query
|
||||||
|
queries, err := query.Values(values)
|
||||||
|
|
||||||
|
// Handle error (no need to check for nginx/headers since we are sure we are using caddy/traefik)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to build queries")
|
||||||
|
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are using caddy/traefik so redirect
|
||||||
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", h.Config.AppURL, queries.Encode()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header("Remote-User", utils.SanitizeHeader(userContext.Username))
|
||||||
|
c.Header("Remote-Name", utils.SanitizeHeader(userContext.Name))
|
||||||
|
c.Header("Remote-Email", utils.SanitizeHeader(userContext.Email))
|
||||||
|
c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups))
|
||||||
|
|
||||||
// Set the rest of the headers
|
// Set the rest of the headers
|
||||||
for key, value := range labels.Headers {
|
for key, value := range labels.Headers {
|
||||||
log.Debug().Str("key", key).Str("value", value).Msg("Setting header")
|
log.Debug().Str("key", key).Str("value", value).Msg("Setting header")
|
||||||
c.Header(key, value)
|
c.Header(key, utils.SanitizeHeader(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
// The user is allowed to access the app
|
// The user is allowed to access the app
|
||||||
@@ -249,12 +301,34 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
|
|||||||
|
|
||||||
log.Debug().Msg("Got login request")
|
log.Debug().Msg("Got login request")
|
||||||
|
|
||||||
|
// Get client IP for rate limiting
|
||||||
|
clientIP := c.ClientIP()
|
||||||
|
|
||||||
|
// Create an identifier for rate limiting (username or IP if username doesn't exist yet)
|
||||||
|
rateIdentifier := login.Username
|
||||||
|
if rateIdentifier == "" {
|
||||||
|
rateIdentifier = clientIP
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the account is locked due to too many failed attempts
|
||||||
|
locked, remainingTime := h.Auth.IsAccountLocked(rateIdentifier)
|
||||||
|
if locked {
|
||||||
|
log.Warn().Str("identifier", rateIdentifier).Int("remaining_seconds", remainingTime).Msg("Account is locked due to too many failed login attempts")
|
||||||
|
c.JSON(429, gin.H{
|
||||||
|
"status": 429,
|
||||||
|
"message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remainingTime),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Get user based on username
|
// Get user based on username
|
||||||
user := h.Auth.GetUser(login.Username)
|
user := h.Auth.GetUser(login.Username)
|
||||||
|
|
||||||
// User does not exist
|
// User does not exist
|
||||||
if user == nil {
|
if user == nil {
|
||||||
log.Debug().Str("username", login.Username).Msg("User not found")
|
log.Debug().Str("username", login.Username).Msg("User not found")
|
||||||
|
// Record failed login attempt
|
||||||
|
h.Auth.RecordLoginAttempt(rateIdentifier, false)
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
"status": 401,
|
"status": 401,
|
||||||
"message": "Unauthorized",
|
"message": "Unauthorized",
|
||||||
@@ -267,6 +341,8 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
|
|||||||
// Check if password is correct
|
// Check if password is correct
|
||||||
if !h.Auth.CheckPassword(*user, login.Password) {
|
if !h.Auth.CheckPassword(*user, login.Password) {
|
||||||
log.Debug().Str("username", login.Username).Msg("Password incorrect")
|
log.Debug().Str("username", login.Username).Msg("Password incorrect")
|
||||||
|
// Record failed login attempt
|
||||||
|
h.Auth.RecordLoginAttempt(rateIdentifier, false)
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
"status": 401,
|
"status": 401,
|
||||||
"message": "Unauthorized",
|
"message": "Unauthorized",
|
||||||
@@ -276,6 +352,9 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
|
|||||||
|
|
||||||
log.Debug().Msg("Password correct, checking totp")
|
log.Debug().Msg("Password correct, checking totp")
|
||||||
|
|
||||||
|
// Record successful login attempt (will reset failed attempt counter)
|
||||||
|
h.Auth.RecordLoginAttempt(rateIdentifier, true)
|
||||||
|
|
||||||
// Check if user has totp enabled
|
// Check if user has totp enabled
|
||||||
if user.TotpSecret != "" {
|
if user.TotpSecret != "" {
|
||||||
log.Debug().Msg("Totp enabled")
|
log.Debug().Msg("Totp enabled")
|
||||||
@@ -283,6 +362,8 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
|
|||||||
// Set totp pending cookie
|
// Set totp pending cookie
|
||||||
h.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
h.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
||||||
Username: login.Username,
|
Username: login.Username,
|
||||||
|
Name: utils.Capitalize(login.Username),
|
||||||
|
Email: fmt.Sprintf("%s@%s", strings.ToLower(login.Username), h.Config.Domain),
|
||||||
Provider: "username",
|
Provider: "username",
|
||||||
TotpPending: true,
|
TotpPending: true,
|
||||||
})
|
})
|
||||||
@@ -301,6 +382,8 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
|
|||||||
// Create session cookie with username as provider
|
// Create session cookie with username as provider
|
||||||
h.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
h.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
||||||
Username: login.Username,
|
Username: login.Username,
|
||||||
|
Name: utils.Capitalize(login.Username),
|
||||||
|
Email: fmt.Sprintf("%s@%s", strings.ToLower(login.Username), h.Config.Domain),
|
||||||
Provider: "username",
|
Provider: "username",
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -375,6 +458,8 @@ func (h *Handlers) TotpHandler(c *gin.Context) {
|
|||||||
// Create session cookie with username as provider
|
// Create session cookie with username as provider
|
||||||
h.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
h.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
|
Name: utils.Capitalize(user.Username),
|
||||||
|
Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), h.Config.Domain),
|
||||||
Provider: "username",
|
Provider: "username",
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -393,9 +478,6 @@ func (h *Handlers) LogoutHandler(c *gin.Context) {
|
|||||||
|
|
||||||
log.Debug().Msg("Cleaning up redirect cookie")
|
log.Debug().Msg("Cleaning up redirect cookie")
|
||||||
|
|
||||||
// Clean up redirect cookie if it exists
|
|
||||||
c.SetCookie("tinyauth_redirect_uri", "", -1, "/", h.Config.Domain, h.Config.CookieSecure, true)
|
|
||||||
|
|
||||||
// Return logged out
|
// Return logged out
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
@@ -416,12 +498,15 @@ func (h *Handlers) AppHandler(c *gin.Context) {
|
|||||||
|
|
||||||
// Create app context struct
|
// Create app context struct
|
||||||
appContext := types.AppContext{
|
appContext := types.AppContext{
|
||||||
Status: 200,
|
Status: 200,
|
||||||
Message: "OK",
|
Message: "OK",
|
||||||
ConfiguredProviders: configuredProviders,
|
ConfiguredProviders: configuredProviders,
|
||||||
DisableContinue: h.Config.DisableContinue,
|
DisableContinue: h.Config.DisableContinue,
|
||||||
Title: h.Config.Title,
|
Title: h.Config.Title,
|
||||||
GenericName: h.Config.GenericName,
|
GenericName: h.Config.GenericName,
|
||||||
|
Domain: h.Config.Domain,
|
||||||
|
ForgotPasswordMessage: h.Config.ForgotPasswordMessage,
|
||||||
|
OAuthAutoRedirect: h.Config.OAuthAutoRedirect,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return app context
|
// Return app context
|
||||||
@@ -439,6 +524,8 @@ func (h *Handlers) UserHandler(c *gin.Context) {
|
|||||||
Status: 200,
|
Status: 200,
|
||||||
IsLoggedIn: userContext.IsLoggedIn,
|
IsLoggedIn: userContext.IsLoggedIn,
|
||||||
Username: userContext.Username,
|
Username: userContext.Username,
|
||||||
|
Name: userContext.Name,
|
||||||
|
Email: userContext.Email,
|
||||||
Provider: userContext.Provider,
|
Provider: userContext.Provider,
|
||||||
Oauth: userContext.OAuth,
|
Oauth: userContext.OAuth,
|
||||||
TotpPending: userContext.TotpPending,
|
TotpPending: userContext.TotpPending,
|
||||||
@@ -491,44 +578,24 @@ func (h *Handlers) OauthUrlHandler(c *gin.Context) {
|
|||||||
|
|
||||||
log.Debug().Str("provider", request.Provider).Msg("Got provider")
|
log.Debug().Str("provider", request.Provider).Msg("Got provider")
|
||||||
|
|
||||||
|
// Create state
|
||||||
|
state := provider.GenerateState()
|
||||||
|
|
||||||
// Get auth URL
|
// Get auth URL
|
||||||
authURL := provider.GetAuthURL()
|
authURL := provider.GetAuthURL(state)
|
||||||
|
|
||||||
log.Debug().Msg("Got auth URL")
|
log.Debug().Msg("Got auth URL")
|
||||||
|
|
||||||
|
// Set CSRF cookie
|
||||||
|
c.SetCookie("tinyauth-csrf", state, int(time.Hour.Seconds()), "/", "", h.Config.CookieSecure, true)
|
||||||
|
|
||||||
// Get redirect URI
|
// Get redirect URI
|
||||||
redirectURI := c.Query("redirect_uri")
|
redirectURI := c.Query("redirect_uri")
|
||||||
|
|
||||||
// Set redirect cookie if redirect URI is provided
|
// Set redirect cookie if redirect URI is provided
|
||||||
if redirectURI != "" {
|
if redirectURI != "" {
|
||||||
log.Debug().Str("redirectURI", redirectURI).Msg("Setting redirect cookie")
|
log.Debug().Str("redirectURI", redirectURI).Msg("Setting redirect cookie")
|
||||||
c.SetCookie("tinyauth_redirect_uri", redirectURI, 3600, "/", h.Config.Domain, h.Config.CookieSecure, true)
|
c.SetCookie("tinyauth-redirect", redirectURI, int(time.Hour.Seconds()), "/", "", h.Config.CookieSecure, true)
|
||||||
}
|
|
||||||
|
|
||||||
// Tailscale does not have an auth url so we create a random code (does not need to be secure) to avoid caching and send it
|
|
||||||
if request.Provider == "tailscale" {
|
|
||||||
// Build tailscale query
|
|
||||||
queries, err := query.Values(types.TailscaleQuery{
|
|
||||||
Code: (1000 + rand.IntN(9000)),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle error
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Failed to build queries")
|
|
||||||
c.JSON(500, gin.H{
|
|
||||||
"status": 500,
|
|
||||||
"message": "Internal Server Error",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return tailscale URL (immidiately redirects to the callback)
|
|
||||||
c.JSON(200, gin.H{
|
|
||||||
"status": 200,
|
|
||||||
"message": "OK",
|
|
||||||
"url": fmt.Sprintf("%s/api/oauth/callback/tailscale?%s", h.Config.AppURL, queries.Encode()),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return auth URL
|
// Return auth URL
|
||||||
@@ -555,16 +622,33 @@ func (h *Handlers) OauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
log.Debug().Interface("provider", providerName.Provider).Msg("Got provider name")
|
log.Debug().Interface("provider", providerName.Provider).Msg("Got provider name")
|
||||||
|
|
||||||
// Get code
|
// Get state
|
||||||
code := c.Query("code")
|
state := c.Query("state")
|
||||||
|
|
||||||
// Code empty so redirect to error
|
// Get CSRF cookie
|
||||||
if code == "" {
|
csrfCookie, err := c.Cookie("tinyauth-csrf")
|
||||||
log.Error().Msg("No code provided")
|
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Msg("No CSRF cookie")
|
||||||
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug().Str("csrfCookie", csrfCookie).Msg("Got CSRF cookie")
|
||||||
|
|
||||||
|
// Check if CSRF cookie is valid
|
||||||
|
if csrfCookie != state {
|
||||||
|
log.Warn().Msg("Invalid CSRF cookie or CSRF cookie does not match with the state")
|
||||||
|
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up CSRF cookie
|
||||||
|
c.SetCookie("tinyauth-csrf", "", -1, "/", "", h.Config.CookieSecure, true)
|
||||||
|
|
||||||
|
// Get code
|
||||||
|
code := c.Query("code")
|
||||||
|
|
||||||
log.Debug().Msg("Got code")
|
log.Debug().Msg("Got code")
|
||||||
|
|
||||||
// Get provider
|
// Get provider
|
||||||
@@ -585,35 +669,42 @@ func (h *Handlers) OauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
// Handle error
|
// Handle error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msg("Failed to exchange token")
|
log.Error().Err(err).Msg("Failed to exchange token")
|
||||||
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get email
|
// Get user
|
||||||
email, err := h.Providers.GetUser(providerName.Provider)
|
user, err := h.Providers.GetUser(providerName.Provider)
|
||||||
|
|
||||||
log.Debug().Str("email", email).Msg("Got email")
|
|
||||||
|
|
||||||
// Handle error
|
// Handle error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msg("Failed to get email")
|
log.Error().Msg("Failed to get user")
|
||||||
|
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Got user")
|
||||||
|
|
||||||
|
// Check that email is not empty
|
||||||
|
if user.Email == "" {
|
||||||
|
log.Error().Msg("Email is empty")
|
||||||
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Email is not whitelisted
|
// Email is not whitelisted
|
||||||
if !h.Auth.EmailWhitelisted(email) {
|
if !h.Auth.EmailWhitelisted(user.Email) {
|
||||||
log.Warn().Str("email", email).Msg("Email not whitelisted")
|
log.Warn().Str("email", user.Email).Msg("Email not whitelisted")
|
||||||
|
|
||||||
// Build query
|
// Build query
|
||||||
queries, err := query.Values(types.UnauthorizedQuery{
|
queries, err := query.Values(types.UnauthorizedQuery{
|
||||||
Username: email,
|
Username: user.Email,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Handle error
|
// Handle error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msg("Failed to build queries")
|
log.Error().Err(err).Msg("Failed to build queries")
|
||||||
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -624,39 +715,61 @@ func (h *Handlers) OauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
log.Debug().Msg("Email whitelisted")
|
log.Debug().Msg("Email whitelisted")
|
||||||
|
|
||||||
// Create session cookie
|
// Get username
|
||||||
h.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
var username string
|
||||||
Username: email,
|
|
||||||
Provider: providerName.Provider,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Get redirect URI
|
if user.PreferredUsername != "" {
|
||||||
redirectURI, err := c.Cookie("tinyauth_redirect_uri")
|
username = user.PreferredUsername
|
||||||
|
} else {
|
||||||
// If it is empty it means that no redirect_uri was provided to the login screen so we just log in
|
username = fmt.Sprintf("%s_%s", strings.Split(user.Email, "@")[0], strings.Split(user.Email, "@")[1])
|
||||||
if err != nil {
|
|
||||||
c.Redirect(http.StatusPermanentRedirect, h.Config.AppURL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Str("redirectURI", redirectURI).Msg("Got redirect URI")
|
// Get name
|
||||||
|
var name string
|
||||||
|
|
||||||
// Clean up redirect cookie since we already have the value
|
if user.Name != "" {
|
||||||
c.SetCookie("tinyauth_redirect_uri", "", -1, "/", h.Config.Domain, h.Config.CookieSecure, true)
|
name = user.Name
|
||||||
|
} else {
|
||||||
|
name = fmt.Sprintf("%s (%s)", utils.Capitalize(strings.Split(user.Email, "@")[0]), strings.Split(user.Email, "@")[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create session cookie (also cleans up redirect cookie)
|
||||||
|
h.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
||||||
|
Username: username,
|
||||||
|
Name: name,
|
||||||
|
Email: user.Email,
|
||||||
|
Provider: providerName.Provider,
|
||||||
|
OAuthGroups: strings.Join(user.Groups, ","),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check if we have a redirect URI
|
||||||
|
redirectCookie, err := c.Cookie("tinyauth-redirect")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Msg("No redirect cookie")
|
||||||
|
c.Redirect(http.StatusPermanentRedirect, h.Config.AppURL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Str("redirectURI", redirectCookie).Msg("Got redirect URI")
|
||||||
|
|
||||||
// Build query
|
// Build query
|
||||||
queries, err := query.Values(types.LoginQuery{
|
queries, err := query.Values(types.LoginQuery{
|
||||||
RedirectURI: redirectURI,
|
RedirectURI: redirectCookie,
|
||||||
})
|
})
|
||||||
|
|
||||||
log.Debug().Msg("Got redirect query")
|
log.Debug().Msg("Got redirect query")
|
||||||
|
|
||||||
// Handle error
|
// Handle error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msg("Failed to build queries")
|
log.Error().Err(err).Msg("Failed to build queries")
|
||||||
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up redirect cookie
|
||||||
|
c.SetCookie("tinyauth-redirect", "", -1, "/", "", h.Config.CookieSecure, true)
|
||||||
|
|
||||||
// Redirect to continue with the redirect URI
|
// Redirect to continue with the redirect URI
|
||||||
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/continue?%s", h.Config.AppURL, queries.Encode()))
|
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/continue?%s", h.Config.AppURL, queries.Encode()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +1,81 @@
|
|||||||
package hooks
|
package hooks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
"tinyauth/internal/auth"
|
"tinyauth/internal/auth"
|
||||||
"tinyauth/internal/providers"
|
"tinyauth/internal/providers"
|
||||||
"tinyauth/internal/types"
|
"tinyauth/internal/types"
|
||||||
|
"tinyauth/internal/utils"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewHooks(auth *auth.Auth, providers *providers.Providers) *Hooks {
|
func NewHooks(config types.HooksConfig, auth *auth.Auth, providers *providers.Providers) *Hooks {
|
||||||
return &Hooks{
|
return &Hooks{
|
||||||
|
Config: config,
|
||||||
Auth: auth,
|
Auth: auth,
|
||||||
Providers: providers,
|
Providers: providers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Hooks struct {
|
type Hooks struct {
|
||||||
|
Config types.HooksConfig
|
||||||
Auth *auth.Auth
|
Auth *auth.Auth
|
||||||
Providers *providers.Providers
|
Providers *providers.Providers
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
|
func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
|
||||||
// Get session cookie and basic auth
|
// Get session cookie and basic auth
|
||||||
cookie := hooks.Auth.GetSessionCookie(c)
|
cookie, err := hooks.Auth.GetSessionCookie(c)
|
||||||
basic := hooks.Auth.GetBasicAuth(c)
|
basic := hooks.Auth.GetBasicAuth(c)
|
||||||
|
|
||||||
// Check if basic auth is set
|
// Check if basic auth is set
|
||||||
if basic != nil {
|
if basic != nil {
|
||||||
log.Debug().Msg("Got basic auth")
|
log.Debug().Msg("Got basic auth")
|
||||||
|
|
||||||
// Check if user exists and password is correct
|
// Get user
|
||||||
user := hooks.Auth.GetUser(basic.Username)
|
user := hooks.Auth.GetUser(basic.Username)
|
||||||
|
|
||||||
if user != nil && hooks.Auth.CheckPassword(*user, basic.Password) {
|
// Check we have a user
|
||||||
|
if user == nil {
|
||||||
|
log.Error().Str("username", basic.Username).Msg("User does not exist")
|
||||||
|
|
||||||
|
// Return empty context
|
||||||
|
return types.UserContext{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user has a correct password
|
||||||
|
if hooks.Auth.CheckPassword(*user, basic.Password) {
|
||||||
// Return user context since we are logged in with basic auth
|
// Return user context since we are logged in with basic auth
|
||||||
return types.UserContext{
|
return types.UserContext{
|
||||||
Username: basic.Username,
|
Username: basic.Username,
|
||||||
|
Name: utils.Capitalize(basic.Username),
|
||||||
|
Email: fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), hooks.Config.Domain),
|
||||||
IsLoggedIn: true,
|
IsLoggedIn: true,
|
||||||
OAuth: false,
|
|
||||||
Provider: "basic",
|
Provider: "basic",
|
||||||
TotpPending: false,
|
TotpEnabled: user.TotpSecret != "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check cookie error after basic auth
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to get session cookie")
|
||||||
|
// Return empty context
|
||||||
|
return types.UserContext{}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if session cookie has totp pending
|
// Check if session cookie has totp pending
|
||||||
if cookie.TotpPending {
|
if cookie.TotpPending {
|
||||||
log.Debug().Msg("Totp pending")
|
log.Debug().Msg("Totp pending")
|
||||||
// Return empty context since we are pending totp
|
// Return empty context since we are pending totp
|
||||||
return types.UserContext{
|
return types.UserContext{
|
||||||
Username: cookie.Username,
|
Username: cookie.Username,
|
||||||
IsLoggedIn: false,
|
Name: cookie.Name,
|
||||||
OAuth: false,
|
Email: cookie.Email,
|
||||||
Provider: cookie.Provider,
|
Provider: cookie.Provider,
|
||||||
TotpPending: true,
|
TotpPending: true,
|
||||||
}
|
}
|
||||||
@@ -69,11 +91,11 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
|
|||||||
|
|
||||||
// It exists so we are logged in
|
// It exists so we are logged in
|
||||||
return types.UserContext{
|
return types.UserContext{
|
||||||
Username: cookie.Username,
|
Username: cookie.Username,
|
||||||
IsLoggedIn: true,
|
Name: cookie.Name,
|
||||||
OAuth: false,
|
Email: cookie.Email,
|
||||||
Provider: "username",
|
IsLoggedIn: true,
|
||||||
TotpPending: false,
|
Provider: "username",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,13 +117,7 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
|
|||||||
hooks.Auth.DeleteSessionCookie(c)
|
hooks.Auth.DeleteSessionCookie(c)
|
||||||
|
|
||||||
// Return empty context
|
// Return empty context
|
||||||
return types.UserContext{
|
return types.UserContext{}
|
||||||
Username: "",
|
|
||||||
IsLoggedIn: false,
|
|
||||||
OAuth: false,
|
|
||||||
Provider: "",
|
|
||||||
TotpPending: false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Email is whitelisted")
|
log.Debug().Msg("Email is whitelisted")
|
||||||
@@ -109,19 +125,15 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
|
|||||||
// Return user context since we are logged in with oauth
|
// Return user context since we are logged in with oauth
|
||||||
return types.UserContext{
|
return types.UserContext{
|
||||||
Username: cookie.Username,
|
Username: cookie.Username,
|
||||||
|
Name: cookie.Name,
|
||||||
|
Email: cookie.Email,
|
||||||
IsLoggedIn: true,
|
IsLoggedIn: true,
|
||||||
OAuth: true,
|
OAuth: true,
|
||||||
Provider: cookie.Provider,
|
Provider: cookie.Provider,
|
||||||
TotpPending: false,
|
OAuthGroups: cookie.OAuthGroups,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Neither basic auth or oauth is set so we return an empty context
|
// Neither basic auth or oauth is set so we return an empty context
|
||||||
return types.UserContext{
|
return types.UserContext{}
|
||||||
Username: "",
|
|
||||||
IsLoggedIn: false,
|
|
||||||
OAuth: false,
|
|
||||||
Provider: "",
|
|
||||||
TotpPending: false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package oauth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
@@ -26,9 +28,9 @@ func (oauth *OAuth) Init() {
|
|||||||
oauth.Verifier = oauth2.GenerateVerifier()
|
oauth.Verifier = oauth2.GenerateVerifier()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (oauth *OAuth) GetAuthURL() string {
|
func (oauth *OAuth) GetAuthURL(state string) string {
|
||||||
// Return the auth url
|
// Return the auth url
|
||||||
return oauth.Config.AuthCodeURL("state", oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(oauth.Verifier))
|
return oauth.Config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(oauth.Verifier))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (oauth *OAuth) ExchangeToken(code string) (string, error) {
|
func (oauth *OAuth) ExchangeToken(code string) (string, error) {
|
||||||
@@ -51,3 +53,16 @@ func (oauth *OAuth) GetClient() *http.Client {
|
|||||||
// Return the http client with the token set
|
// Return the http client with the token set
|
||||||
return oauth.Config.Client(oauth.Context, oauth.Token)
|
return oauth.Config.Client(oauth.Context, oauth.Token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (oauth *OAuth) GenerateState() string {
|
||||||
|
// Generate a random state string
|
||||||
|
b := make([]byte, 128)
|
||||||
|
|
||||||
|
// Fill the byte slice with random data
|
||||||
|
rand.Read(b)
|
||||||
|
|
||||||
|
// Encode the byte slice to a base64 string
|
||||||
|
state := base64.URLEncoding.EncodeToString(b)
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,24 +4,25 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"tinyauth/internal/constants"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// We are assuming that the generic provider will return a JSON object with an email field
|
func GetGenericUser(client *http.Client, url string) (constants.Claims, error) {
|
||||||
type GenericUserInfoResponse struct {
|
// Create user struct
|
||||||
Email string `json:"email"`
|
var user constants.Claims
|
||||||
}
|
|
||||||
|
|
||||||
func GetGenericEmail(client *http.Client, url string) (string, error) {
|
|
||||||
// Using the oauth client get the user info url
|
// Using the oauth client get the user info url
|
||||||
res, err := client.Get(url)
|
res, err := client.Get(url)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
log.Debug().Msg("Got response from generic provider")
|
log.Debug().Msg("Got response from generic provider")
|
||||||
|
|
||||||
// Read the body of the response
|
// Read the body of the response
|
||||||
@@ -29,24 +30,21 @@ func GetGenericEmail(client *http.Client, url string) (string, error) {
|
|||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Read body from generic provider")
|
log.Debug().Msg("Read body from generic provider")
|
||||||
|
|
||||||
// Parse the body into a user struct
|
|
||||||
var user GenericUserInfoResponse
|
|
||||||
|
|
||||||
// Unmarshal the body into the user struct
|
// Unmarshal the body into the user struct
|
||||||
err = json.Unmarshal(body, &user)
|
err = json.Unmarshal(body, &user)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Parsed user from generic provider")
|
log.Debug().Msg("Parsed user from generic provider")
|
||||||
|
|
||||||
// Return the email
|
// Return the user
|
||||||
return user.Email, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,51 +5,96 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"tinyauth/internal/constants"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Github has a different response than the generic provider
|
// Response for the github email endpoint
|
||||||
type GithubUserInfoResponse []struct {
|
type GithubEmailResponse []struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Primary bool `json:"primary"`
|
Primary bool `json:"primary"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// The scopes required for the github provider
|
// Response for the github user endpoint
|
||||||
func GithubScopes() []string {
|
type GithubUserInfoResponse struct {
|
||||||
return []string{"user:email"}
|
Login string `json:"login"`
|
||||||
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetGithubEmail(client *http.Client) (string, error) {
|
// The scopes required for the github provider
|
||||||
// Get the user emails from github using the oauth http client
|
func GithubScopes() []string {
|
||||||
res, err := client.Get("https://api.github.com/user/emails")
|
return []string{"user:email", "read:user"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetGithubUser(client *http.Client) (constants.Claims, error) {
|
||||||
|
// Create user struct
|
||||||
|
var user constants.Claims
|
||||||
|
|
||||||
|
// Get the user info from github using the oauth http client
|
||||||
|
res, err := client.Get("https://api.github.com/user")
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Got response from github")
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
log.Debug().Msg("Got user response from github")
|
||||||
|
|
||||||
// Read the body of the response
|
// Read the body of the response
|
||||||
body, err := io.ReadAll(res.Body)
|
body, err := io.ReadAll(res.Body)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Read body from github")
|
log.Debug().Msg("Read user body from github")
|
||||||
|
|
||||||
// Parse the body into a user struct
|
// Parse the body into a user struct
|
||||||
var emails GithubUserInfoResponse
|
var userInfo GithubUserInfoResponse
|
||||||
|
|
||||||
|
// Unmarshal the body into the user struct
|
||||||
|
err = json.Unmarshal(body, &userInfo)
|
||||||
|
|
||||||
|
// Check if there was an error
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the user emails from github using the oauth http client
|
||||||
|
res, err = client.Get("https://api.github.com/user/emails")
|
||||||
|
|
||||||
|
// Check if there was an error
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
log.Debug().Msg("Got email response from github")
|
||||||
|
|
||||||
|
// Read the body of the response
|
||||||
|
body, err = io.ReadAll(res.Body)
|
||||||
|
|
||||||
|
// Check if there was an error
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Read email body from github")
|
||||||
|
|
||||||
|
// Parse the body into a user struct
|
||||||
|
var emails GithubEmailResponse
|
||||||
|
|
||||||
// Unmarshal the body into the user struct
|
// Unmarshal the body into the user struct
|
||||||
err = json.Unmarshal(body, &emails)
|
err = json.Unmarshal(body, &emails)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Parsed emails from github")
|
log.Debug().Msg("Parsed emails from github")
|
||||||
@@ -57,10 +102,26 @@ func GetGithubEmail(client *http.Client) (string, error) {
|
|||||||
// Find and return the primary email
|
// Find and return the primary email
|
||||||
for _, email := range emails {
|
for _, email := range emails {
|
||||||
if email.Primary {
|
if email.Primary {
|
||||||
return email.Email, nil
|
// Set the email then exit
|
||||||
|
user.Email = email.Email
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// User does not have a primary email?
|
// If no primary email was found, use the first available email
|
||||||
return "", errors.New("no primary email found")
|
if len(emails) == 0 {
|
||||||
|
return user, errors.New("no emails found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the email if it is not set picking the first one
|
||||||
|
if user.Email == "" {
|
||||||
|
user.Email = emails[0].Email
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the username and name
|
||||||
|
user.PreferredUsername = userInfo.Login
|
||||||
|
user.Name = userInfo.Name
|
||||||
|
|
||||||
|
// Return
|
||||||
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,29 +4,37 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"tinyauth/internal/constants"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Google works the same as the generic provider
|
// Response for the google user endpoint
|
||||||
type GoogleUserInfoResponse struct {
|
type GoogleUserInfoResponse struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// The scopes required for the google provider
|
// The scopes required for the google provider
|
||||||
func GoogleScopes() []string {
|
func GoogleScopes() []string {
|
||||||
return []string{"https://www.googleapis.com/auth/userinfo.email"}
|
return []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetGoogleEmail(client *http.Client) (string, error) {
|
func GetGoogleUser(client *http.Client) (constants.Claims, error) {
|
||||||
|
// Create user struct
|
||||||
|
var user constants.Claims
|
||||||
|
|
||||||
// Get the user info from google using the oauth http client
|
// Get the user info from google using the oauth http client
|
||||||
res, err := client.Get("https://www.googleapis.com/userinfo/v2/me")
|
res, err := client.Get("https://www.googleapis.com/userinfo/v2/me")
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
log.Debug().Msg("Got response from google")
|
log.Debug().Msg("Got response from google")
|
||||||
|
|
||||||
// Read the body of the response
|
// Read the body of the response
|
||||||
@@ -34,24 +42,29 @@ func GetGoogleEmail(client *http.Client) (string, error) {
|
|||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Read body from google")
|
log.Debug().Msg("Read body from google")
|
||||||
|
|
||||||
// Parse the body into a user struct
|
// Create a new user info struct
|
||||||
var user GoogleUserInfoResponse
|
var userInfo GoogleUserInfoResponse
|
||||||
|
|
||||||
// Unmarshal the body into the user struct
|
// Unmarshal the body into the user struct
|
||||||
err = json.Unmarshal(body, &user)
|
err = json.Unmarshal(body, &userInfo)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Parsed user from google")
|
log.Debug().Msg("Parsed user from google")
|
||||||
|
|
||||||
// Return the email
|
// Map the user info to the user struct
|
||||||
return user.Email, nil
|
user.PreferredUsername = strings.Split(userInfo.Email, "@")[0]
|
||||||
|
user.Name = userInfo.Name
|
||||||
|
user.Email = userInfo.Email
|
||||||
|
|
||||||
|
// Return the user
|
||||||
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package providers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"tinyauth/internal/constants"
|
||||||
"tinyauth/internal/oauth"
|
"tinyauth/internal/oauth"
|
||||||
"tinyauth/internal/types"
|
"tinyauth/internal/types"
|
||||||
|
|
||||||
@@ -17,11 +18,10 @@ func NewProviders(config types.OAuthConfig) *Providers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Providers struct {
|
type Providers struct {
|
||||||
Config types.OAuthConfig
|
Config types.OAuthConfig
|
||||||
Github *oauth.OAuth
|
Github *oauth.OAuth
|
||||||
Google *oauth.OAuth
|
Google *oauth.OAuth
|
||||||
Tailscale *oauth.OAuth
|
Generic *oauth.OAuth
|
||||||
Generic *oauth.OAuth
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (providers *Providers) Init() {
|
func (providers *Providers) Init() {
|
||||||
@@ -59,22 +59,6 @@ func (providers *Providers) Init() {
|
|||||||
providers.Google.Init()
|
providers.Google.Init()
|
||||||
}
|
}
|
||||||
|
|
||||||
if providers.Config.TailscaleClientId != "" && providers.Config.TailscaleClientSecret != "" {
|
|
||||||
log.Info().Msg("Initializing Tailscale OAuth")
|
|
||||||
|
|
||||||
// Create a new oauth provider with the tailscale config
|
|
||||||
providers.Tailscale = oauth.NewOAuth(oauth2.Config{
|
|
||||||
ClientID: providers.Config.TailscaleClientId,
|
|
||||||
ClientSecret: providers.Config.TailscaleClientSecret,
|
|
||||||
RedirectURL: fmt.Sprintf("%s/api/oauth/callback/tailscale", providers.Config.AppURL),
|
|
||||||
Scopes: TailscaleScopes(),
|
|
||||||
Endpoint: TailscaleEndpoint,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Initialize the oauth provider
|
|
||||||
providers.Tailscale.Init()
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have a client id and secret for generic oauth, initialize the oauth provider
|
// If we have a client id and secret for generic oauth, initialize the oauth provider
|
||||||
if providers.Config.GenericClientId != "" && providers.Config.GenericClientSecret != "" {
|
if providers.Config.GenericClientId != "" && providers.Config.GenericClientSecret != "" {
|
||||||
log.Info().Msg("Initializing Generic OAuth")
|
log.Info().Msg("Initializing Generic OAuth")
|
||||||
@@ -103,8 +87,6 @@ func (providers *Providers) GetProvider(provider string) *oauth.OAuth {
|
|||||||
return providers.Github
|
return providers.Github
|
||||||
case "google":
|
case "google":
|
||||||
return providers.Google
|
return providers.Google
|
||||||
case "tailscale":
|
|
||||||
return providers.Tailscale
|
|
||||||
case "generic":
|
case "generic":
|
||||||
return providers.Generic
|
return providers.Generic
|
||||||
default:
|
default:
|
||||||
@@ -112,14 +94,17 @@ func (providers *Providers) GetProvider(provider string) *oauth.OAuth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (providers *Providers) GetUser(provider string) (string, error) {
|
func (providers *Providers) GetUser(provider string) (constants.Claims, error) {
|
||||||
// Get the email from the provider
|
// Create user struct
|
||||||
|
var user constants.Claims
|
||||||
|
|
||||||
|
// Get the user from the provider
|
||||||
switch provider {
|
switch provider {
|
||||||
case "github":
|
case "github":
|
||||||
// If the github provider is not configured, return an error
|
// If the github provider is not configured, return an error
|
||||||
if providers.Github == nil {
|
if providers.Github == nil {
|
||||||
log.Debug().Msg("Github provider not configured")
|
log.Debug().Msg("Github provider not configured")
|
||||||
return "", nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the client from the github provider
|
// Get the client from the github provider
|
||||||
@@ -127,23 +112,23 @@ func (providers *Providers) GetUser(provider string) (string, error) {
|
|||||||
|
|
||||||
log.Debug().Msg("Got client from github")
|
log.Debug().Msg("Got client from github")
|
||||||
|
|
||||||
// Get the email from the github provider
|
// Get the user from the github provider
|
||||||
email, err := GetGithubEmail(client)
|
user, err := GetGithubUser(client)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Got email from github")
|
log.Debug().Msg("Got user from github")
|
||||||
|
|
||||||
// Return the email
|
// Return the user
|
||||||
return email, nil
|
return user, nil
|
||||||
case "google":
|
case "google":
|
||||||
// If the google provider is not configured, return an error
|
// If the google provider is not configured, return an error
|
||||||
if providers.Google == nil {
|
if providers.Google == nil {
|
||||||
log.Debug().Msg("Google provider not configured")
|
log.Debug().Msg("Google provider not configured")
|
||||||
return "", nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the client from the google provider
|
// Get the client from the google provider
|
||||||
@@ -151,47 +136,23 @@ func (providers *Providers) GetUser(provider string) (string, error) {
|
|||||||
|
|
||||||
log.Debug().Msg("Got client from google")
|
log.Debug().Msg("Got client from google")
|
||||||
|
|
||||||
// Get the email from the google provider
|
// Get the user from the google provider
|
||||||
email, err := GetGoogleEmail(client)
|
user, err := GetGoogleUser(client)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Got email from google")
|
log.Debug().Msg("Got user from google")
|
||||||
|
|
||||||
// Return the email
|
// Return the user
|
||||||
return email, nil
|
return user, nil
|
||||||
case "tailscale":
|
|
||||||
// If the tailscale provider is not configured, return an error
|
|
||||||
if providers.Tailscale == nil {
|
|
||||||
log.Debug().Msg("Tailscale provider not configured")
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the client from the tailscale provider
|
|
||||||
client := providers.Tailscale.GetClient()
|
|
||||||
|
|
||||||
log.Debug().Msg("Got client from tailscale")
|
|
||||||
|
|
||||||
// Get the email from the tailscale provider
|
|
||||||
email, err := GetTailscaleEmail(client)
|
|
||||||
|
|
||||||
// Check if there was an error
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Msg("Got email from tailscale")
|
|
||||||
|
|
||||||
// Return the email
|
|
||||||
return email, nil
|
|
||||||
case "generic":
|
case "generic":
|
||||||
// If the generic provider is not configured, return an error
|
// If the generic provider is not configured, return an error
|
||||||
if providers.Generic == nil {
|
if providers.Generic == nil {
|
||||||
log.Debug().Msg("Generic provider not configured")
|
log.Debug().Msg("Generic provider not configured")
|
||||||
return "", nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the client from the generic provider
|
// Get the client from the generic provider
|
||||||
@@ -199,20 +160,20 @@ func (providers *Providers) GetUser(provider string) (string, error) {
|
|||||||
|
|
||||||
log.Debug().Msg("Got client from generic")
|
log.Debug().Msg("Got client from generic")
|
||||||
|
|
||||||
// Get the email from the generic provider
|
// Get the user from the generic provider
|
||||||
email, err := GetGenericEmail(client, providers.Config.GenericUserURL)
|
user, err := GetGenericUser(client, providers.Config.GenericUserURL)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Got email from generic")
|
log.Debug().Msg("Got user from generic")
|
||||||
|
|
||||||
// Return the email
|
// Return the email
|
||||||
return email, nil
|
return user, nil
|
||||||
default:
|
default:
|
||||||
return "", nil
|
return user, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,9 +186,6 @@ func (provider *Providers) GetConfiguredProviders() []string {
|
|||||||
if provider.Google != nil {
|
if provider.Google != nil {
|
||||||
providers = append(providers, "google")
|
providers = append(providers, "google")
|
||||||
}
|
}
|
||||||
if provider.Tailscale != nil {
|
|
||||||
providers = append(providers, "tailscale")
|
|
||||||
}
|
|
||||||
if provider.Generic != nil {
|
if provider.Generic != nil {
|
||||||
providers = append(providers, "generic")
|
providers = append(providers, "generic")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
package providers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The tailscale email is the loginName
|
|
||||||
type TailscaleUser struct {
|
|
||||||
LoginName string `json:"loginName"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// The response from the tailscale user info endpoint
|
|
||||||
type TailscaleUserInfoResponse struct {
|
|
||||||
Users []TailscaleUser `json:"users"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// The scopes required for the tailscale provider
|
|
||||||
func TailscaleScopes() []string {
|
|
||||||
return []string{"users:read"}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The tailscale endpoint
|
|
||||||
var TailscaleEndpoint = oauth2.Endpoint{
|
|
||||||
TokenURL: "https://api.tailscale.com/api/v2/oauth/token",
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetTailscaleEmail(client *http.Client) (string, error) {
|
|
||||||
// Get the user info from tailscale using the oauth http client
|
|
||||||
res, err := client.Get("https://api.tailscale.com/api/v2/tailnet/-/users")
|
|
||||||
|
|
||||||
// Check if there was an error
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Msg("Got response from tailscale")
|
|
||||||
|
|
||||||
// Read the body of the response
|
|
||||||
body, err := io.ReadAll(res.Body)
|
|
||||||
|
|
||||||
// Check if there was an error
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Msg("Read body from tailscale")
|
|
||||||
|
|
||||||
// Parse the body into a user struct
|
|
||||||
var users TailscaleUserInfoResponse
|
|
||||||
|
|
||||||
// Unmarshal the body into the user struct
|
|
||||||
err = json.Unmarshal(body, &users)
|
|
||||||
|
|
||||||
// Check if there was an error
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Msg("Parsed users from tailscale")
|
|
||||||
|
|
||||||
// Return the email of the first user
|
|
||||||
return users.Users[0].LoginName, nil
|
|
||||||
}
|
|
||||||
60
internal/types/api.go
Normal file
60
internal/types/api.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
// LoginQuery is the query parameters for the login endpoint
|
||||||
|
type LoginQuery struct {
|
||||||
|
RedirectURI string `url:"redirect_uri"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginRequest is the request body for the login endpoint
|
||||||
|
type LoginRequest struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuthRequest is the request for the OAuth endpoint
|
||||||
|
type OAuthRequest struct {
|
||||||
|
Provider string `uri:"provider" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnauthorizedQuery is the query parameters for the unauthorized endpoint
|
||||||
|
type UnauthorizedQuery struct {
|
||||||
|
Username string `url:"username"`
|
||||||
|
Resource string `url:"resource"`
|
||||||
|
GroupErr bool `url:"groupErr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proxy is the uri parameters for the proxy endpoint
|
||||||
|
type Proxy struct {
|
||||||
|
Proxy string `uri:"proxy" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// User Context response is the response for the user context endpoint
|
||||||
|
type UserContextResponse struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
IsLoggedIn bool `json:"isLoggedIn"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
Oauth bool `json:"oauth"`
|
||||||
|
TotpPending bool `json:"totpPending"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// App Context is the response for the app context endpoint
|
||||||
|
type AppContext struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
ConfiguredProviders []string `json:"configuredProviders"`
|
||||||
|
DisableContinue bool `json:"disableContinue"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
GenericName string `json:"genericName"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
ForgotPasswordMessage string `json:"forgotPasswordMessage"`
|
||||||
|
OAuthAutoRedirect string `json:"oauthAutoRedirect"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Totp request is the request for the totp endpoint
|
||||||
|
type TotpRequest struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
}
|
||||||
87
internal/types/config.go
Normal file
87
internal/types/config.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
// Config is the configuration for the tinyauth server
|
||||||
|
type Config struct {
|
||||||
|
Port int `mapstructure:"port" validate:"required"`
|
||||||
|
Address string `validate:"required,ip4_addr" mapstructure:"address"`
|
||||||
|
Secret string `validate:"required,len=32" mapstructure:"secret"`
|
||||||
|
SecretFile string `mapstructure:"secret-file"`
|
||||||
|
AppURL string `validate:"required,url" mapstructure:"app-url"`
|
||||||
|
Users string `mapstructure:"users"`
|
||||||
|
UsersFile string `mapstructure:"users-file"`
|
||||||
|
CookieSecure bool `mapstructure:"cookie-secure"`
|
||||||
|
GithubClientId string `mapstructure:"github-client-id"`
|
||||||
|
GithubClientSecret string `mapstructure:"github-client-secret"`
|
||||||
|
GithubClientSecretFile string `mapstructure:"github-client-secret-file"`
|
||||||
|
GoogleClientId string `mapstructure:"google-client-id"`
|
||||||
|
GoogleClientSecret string `mapstructure:"google-client-secret"`
|
||||||
|
GoogleClientSecretFile string `mapstructure:"google-client-secret-file"`
|
||||||
|
GenericClientId string `mapstructure:"generic-client-id"`
|
||||||
|
GenericClientSecret string `mapstructure:"generic-client-secret"`
|
||||||
|
GenericClientSecretFile string `mapstructure:"generic-client-secret-file"`
|
||||||
|
GenericScopes string `mapstructure:"generic-scopes"`
|
||||||
|
GenericAuthURL string `mapstructure:"generic-auth-url"`
|
||||||
|
GenericTokenURL string `mapstructure:"generic-token-url"`
|
||||||
|
GenericUserURL string `mapstructure:"generic-user-url"`
|
||||||
|
GenericName string `mapstructure:"generic-name"`
|
||||||
|
DisableContinue bool `mapstructure:"disable-continue"`
|
||||||
|
OAuthWhitelist string `mapstructure:"oauth-whitelist"`
|
||||||
|
OAuthAutoRedirect string `mapstructure:"oauth-auto-redirect" validate:"oneof=none github google generic"`
|
||||||
|
SessionExpiry int `mapstructure:"session-expiry"`
|
||||||
|
LogLevel int8 `mapstructure:"log-level" validate:"min=-1,max=5"`
|
||||||
|
Title string `mapstructure:"app-title"`
|
||||||
|
EnvFile string `mapstructure:"env-file"`
|
||||||
|
LoginTimeout int `mapstructure:"login-timeout"`
|
||||||
|
LoginMaxRetries int `mapstructure:"login-max-retries"`
|
||||||
|
FogotPasswordMessage string `mapstructure:"forgot-password-message" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server configuration
|
||||||
|
type HandlersConfig struct {
|
||||||
|
AppURL string
|
||||||
|
Domain string
|
||||||
|
CookieSecure bool
|
||||||
|
DisableContinue bool
|
||||||
|
GenericName string
|
||||||
|
Title string
|
||||||
|
ForgotPasswordMessage string
|
||||||
|
OAuthAutoRedirect string
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuthConfig is the configuration for the providers
|
||||||
|
type OAuthConfig struct {
|
||||||
|
GithubClientId string
|
||||||
|
GithubClientSecret string
|
||||||
|
GoogleClientId string
|
||||||
|
GoogleClientSecret string
|
||||||
|
GenericClientId string
|
||||||
|
GenericClientSecret string
|
||||||
|
GenericScopes []string
|
||||||
|
GenericAuthURL string
|
||||||
|
GenericTokenURL string
|
||||||
|
GenericUserURL string
|
||||||
|
AppURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIConfig is the configuration for the API
|
||||||
|
type APIConfig struct {
|
||||||
|
Port int
|
||||||
|
Address string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthConfig is the configuration for the auth service
|
||||||
|
type AuthConfig struct {
|
||||||
|
Users Users
|
||||||
|
OauthWhitelist string
|
||||||
|
SessionExpiry int
|
||||||
|
Secret string
|
||||||
|
CookieSecure bool
|
||||||
|
Domain string
|
||||||
|
LoginTimeout int
|
||||||
|
LoginMaxRetries int
|
||||||
|
}
|
||||||
|
|
||||||
|
// HooksConfig is the configuration for the hooks service
|
||||||
|
type HooksConfig struct {
|
||||||
|
Domain string
|
||||||
|
}
|
||||||
@@ -1,17 +1,9 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import "tinyauth/internal/oauth"
|
import (
|
||||||
|
"time"
|
||||||
// LoginQuery is the query parameters for the login endpoint
|
"tinyauth/internal/oauth"
|
||||||
type LoginQuery struct {
|
)
|
||||||
RedirectURI string `url:"redirect_uri"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoginRequest is the request body for the login endpoint
|
|
||||||
type LoginRequest struct {
|
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// User is the struct for a user
|
// User is the struct for a user
|
||||||
type User struct {
|
type User struct {
|
||||||
@@ -23,82 +15,6 @@ type User struct {
|
|||||||
// Users is a list of users
|
// Users is a list of users
|
||||||
type Users []User
|
type Users []User
|
||||||
|
|
||||||
// Config is the configuration for the tinyauth server
|
|
||||||
type Config struct {
|
|
||||||
Port int `mapstructure:"port" validate:"required"`
|
|
||||||
Address string `validate:"required,ip4_addr" mapstructure:"address"`
|
|
||||||
Secret string `validate:"required,len=32" mapstructure:"secret"`
|
|
||||||
SecretFile string `mapstructure:"secret-file"`
|
|
||||||
AppURL string `validate:"required,url" mapstructure:"app-url"`
|
|
||||||
Users string `mapstructure:"users"`
|
|
||||||
UsersFile string `mapstructure:"users-file"`
|
|
||||||
CookieSecure bool `mapstructure:"cookie-secure"`
|
|
||||||
GithubClientId string `mapstructure:"github-client-id"`
|
|
||||||
GithubClientSecret string `mapstructure:"github-client-secret"`
|
|
||||||
GithubClientSecretFile string `mapstructure:"github-client-secret-file"`
|
|
||||||
GoogleClientId string `mapstructure:"google-client-id"`
|
|
||||||
GoogleClientSecret string `mapstructure:"google-client-secret"`
|
|
||||||
GoogleClientSecretFile string `mapstructure:"google-client-secret-file"`
|
|
||||||
TailscaleClientId string `mapstructure:"tailscale-client-id"`
|
|
||||||
TailscaleClientSecret string `mapstructure:"tailscale-client-secret"`
|
|
||||||
TailscaleClientSecretFile string `mapstructure:"tailscale-client-secret-file"`
|
|
||||||
GenericClientId string `mapstructure:"generic-client-id"`
|
|
||||||
GenericClientSecret string `mapstructure:"generic-client-secret"`
|
|
||||||
GenericClientSecretFile string `mapstructure:"generic-client-secret-file"`
|
|
||||||
GenericScopes string `mapstructure:"generic-scopes"`
|
|
||||||
GenericAuthURL string `mapstructure:"generic-auth-url"`
|
|
||||||
GenericTokenURL string `mapstructure:"generic-token-url"`
|
|
||||||
GenericUserURL string `mapstructure:"generic-user-url"`
|
|
||||||
GenericName string `mapstructure:"generic-name"`
|
|
||||||
DisableContinue bool `mapstructure:"disable-continue"`
|
|
||||||
OAuthWhitelist string `mapstructure:"oauth-whitelist"`
|
|
||||||
SessionExpiry int `mapstructure:"session-expiry"`
|
|
||||||
LogLevel int8 `mapstructure:"log-level" validate:"min=-1,max=5"`
|
|
||||||
Title string `mapstructure:"app-title"`
|
|
||||||
EnvFile string `mapstructure:"env-file"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserContext is the context for the user
|
|
||||||
type UserContext struct {
|
|
||||||
Username string
|
|
||||||
IsLoggedIn bool
|
|
||||||
OAuth bool
|
|
||||||
Provider string
|
|
||||||
TotpPending bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// APIConfig is the configuration for the API
|
|
||||||
type APIConfig struct {
|
|
||||||
Port int
|
|
||||||
Address string
|
|
||||||
Secret string
|
|
||||||
CookieSecure bool
|
|
||||||
SessionExpiry int
|
|
||||||
Domain string
|
|
||||||
}
|
|
||||||
|
|
||||||
// OAuthConfig is the configuration for the providers
|
|
||||||
type OAuthConfig struct {
|
|
||||||
GithubClientId string
|
|
||||||
GithubClientSecret string
|
|
||||||
GoogleClientId string
|
|
||||||
GoogleClientSecret string
|
|
||||||
TailscaleClientId string
|
|
||||||
TailscaleClientSecret string
|
|
||||||
GenericClientId string
|
|
||||||
GenericClientSecret string
|
|
||||||
GenericScopes []string
|
|
||||||
GenericAuthURL string
|
|
||||||
GenericTokenURL string
|
|
||||||
GenericUserURL string
|
|
||||||
AppURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
// OAuthRequest is the request for the OAuth endpoint
|
|
||||||
type OAuthRequest struct {
|
|
||||||
Provider string `uri:"provider" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OAuthProviders is the struct for the OAuth providers
|
// OAuthProviders is the struct for the OAuth providers
|
||||||
type OAuthProviders struct {
|
type OAuthProviders struct {
|
||||||
Github *oauth.OAuth
|
Github *oauth.OAuth
|
||||||
@@ -106,69 +22,41 @@ type OAuthProviders struct {
|
|||||||
Microsoft *oauth.OAuth
|
Microsoft *oauth.OAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnauthorizedQuery is the query parameters for the unauthorized endpoint
|
|
||||||
type UnauthorizedQuery struct {
|
|
||||||
Username string `url:"username"`
|
|
||||||
Resource string `url:"resource"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SessionCookie is the cookie for the session (exculding the expiry)
|
// SessionCookie is the cookie for the session (exculding the expiry)
|
||||||
type SessionCookie struct {
|
type SessionCookie struct {
|
||||||
Username string
|
Username string
|
||||||
|
Name string
|
||||||
|
Email string
|
||||||
Provider string
|
Provider string
|
||||||
TotpPending bool
|
TotpPending bool
|
||||||
|
OAuthGroups string
|
||||||
}
|
}
|
||||||
|
|
||||||
// TinyauthLabels is the labels for the tinyauth container
|
// TinyauthLabels is the labels for the tinyauth container
|
||||||
type TinyauthLabels struct {
|
type TinyauthLabels struct {
|
||||||
OAuthWhitelist []string
|
OAuthWhitelist string
|
||||||
Users []string
|
Users string
|
||||||
Allowed string
|
Allowed string
|
||||||
Headers map[string]string
|
Headers map[string]string
|
||||||
|
OAuthGroups string
|
||||||
}
|
}
|
||||||
|
|
||||||
// TailscaleQuery is the query parameters for the tailscale endpoint
|
// UserContext is the context for the user
|
||||||
type TailscaleQuery struct {
|
type UserContext struct {
|
||||||
Code int `url:"code"`
|
Username string
|
||||||
|
Name string
|
||||||
|
Email string
|
||||||
|
IsLoggedIn bool
|
||||||
|
OAuth bool
|
||||||
|
Provider string
|
||||||
|
TotpPending bool
|
||||||
|
OAuthGroups string
|
||||||
|
TotpEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proxy is the uri parameters for the proxy endpoint
|
// LoginAttempt tracks information about login attempts for rate limiting
|
||||||
type Proxy struct {
|
type LoginAttempt struct {
|
||||||
Proxy string `uri:"proxy" binding:"required"`
|
FailedAttempts int
|
||||||
}
|
LastAttempt time.Time
|
||||||
|
LockedUntil time.Time
|
||||||
// User Context response is the response for the user context endpoint
|
|
||||||
type UserContextResponse struct {
|
|
||||||
Status int `json:"status"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
IsLoggedIn bool `json:"isLoggedIn"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
Provider string `json:"provider"`
|
|
||||||
Oauth bool `json:"oauth"`
|
|
||||||
TotpPending bool `json:"totpPending"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// App Context is the response for the app context endpoint
|
|
||||||
type AppContext struct {
|
|
||||||
Status int `json:"status"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
ConfiguredProviders []string `json:"configuredProviders"`
|
|
||||||
DisableContinue bool `json:"disableContinue"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
GenericName string `json:"genericName"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Totp request is the request for the totp endpoint
|
|
||||||
type TotpRequest struct {
|
|
||||||
Code string `json:"code"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server configuration
|
|
||||||
type HandlersConfig struct {
|
|
||||||
AppURL string
|
|
||||||
Domain string
|
|
||||||
CookieSecure bool
|
|
||||||
DisableContinue bool
|
|
||||||
GenericName string
|
|
||||||
Title string
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"tinyauth/internal/constants"
|
"tinyauth/internal/constants"
|
||||||
@@ -130,7 +131,7 @@ func GetSecret(conf string, file string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return the contents of the file
|
// Return the contents of the file
|
||||||
return contents
|
return ParseSecretFile(contents)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the users from the config or file
|
// Get the users from the config or file
|
||||||
@@ -188,9 +189,9 @@ func GetTinyauthLabels(labels map[string]string) types.TinyauthLabels {
|
|||||||
// Add the label value to the tinyauth labels struct
|
// Add the label value to the tinyauth labels struct
|
||||||
switch label {
|
switch label {
|
||||||
case "tinyauth.oauth.whitelist":
|
case "tinyauth.oauth.whitelist":
|
||||||
tinyauthLabels.OAuthWhitelist = strings.Split(value, ",")
|
tinyauthLabels.OAuthWhitelist = value
|
||||||
case "tinyauth.users":
|
case "tinyauth.users":
|
||||||
tinyauthLabels.Users = strings.Split(value, ",")
|
tinyauthLabels.Users = value
|
||||||
case "tinyauth.allowed":
|
case "tinyauth.allowed":
|
||||||
tinyauthLabels.Allowed = value
|
tinyauthLabels.Allowed = value
|
||||||
case "tinyauth.headers":
|
case "tinyauth.headers":
|
||||||
@@ -203,6 +204,8 @@ func GetTinyauthLabels(labels map[string]string) types.TinyauthLabels {
|
|||||||
}
|
}
|
||||||
tinyauthLabels.Headers[headerSplit[0]] = headerSplit[1]
|
tinyauthLabels.Headers[headerSplit[0]] = headerSplit[1]
|
||||||
}
|
}
|
||||||
|
case "tinyauth.oauth.groups":
|
||||||
|
tinyauthLabels.OAuthGroups = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,7 +216,7 @@ func GetTinyauthLabels(labels map[string]string) types.TinyauthLabels {
|
|||||||
|
|
||||||
// Check if any of the OAuth providers are configured based on the client id and secret
|
// Check if any of the OAuth providers are configured based on the client id and secret
|
||||||
func OAuthConfigured(config types.Config) bool {
|
func OAuthConfigured(config types.Config) bool {
|
||||||
return (config.GithubClientId != "" && config.GithubClientSecret != "") || (config.GoogleClientId != "" && config.GoogleClientSecret != "") || (config.GenericClientId != "" && config.GenericClientSecret != "") || (config.TailscaleClientId != "" && config.TailscaleClientSecret != "")
|
return (config.GithubClientId != "" && config.GithubClientSecret != "") || (config.GoogleClientId != "" && config.GoogleClientSecret != "") || (config.GenericClientId != "" && config.GenericClientSecret != "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter helper function
|
// Filter helper function
|
||||||
@@ -241,23 +244,21 @@ func ParseUser(user string) (types.User, error) {
|
|||||||
return types.User{}, errors.New("invalid user format")
|
return types.User{}, errors.New("invalid user format")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the user has a totp secret
|
// Check for empty strings
|
||||||
if len(userSplit) == 2 {
|
for _, userPart := range userSplit {
|
||||||
// Check for empty username or password
|
if strings.TrimSpace(userPart) == "" {
|
||||||
if userSplit[1] == "" || userSplit[0] == "" {
|
|
||||||
return types.User{}, errors.New("invalid user format")
|
return types.User{}, errors.New("invalid user format")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user has a totp secret
|
||||||
|
if len(userSplit) == 2 {
|
||||||
return types.User{
|
return types.User{
|
||||||
Username: userSplit[0],
|
Username: userSplit[0],
|
||||||
Password: userSplit[1],
|
Password: userSplit[1],
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for empty username, password or totp secret
|
|
||||||
if userSplit[2] == "" || userSplit[1] == "" || userSplit[0] == "" {
|
|
||||||
return types.User{}, errors.New("invalid user format")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the user struct
|
// Return the user struct
|
||||||
return types.User{
|
return types.User{
|
||||||
Username: userSplit[0],
|
Username: userSplit[0],
|
||||||
@@ -265,3 +266,81 @@ func ParseUser(user string) (types.User, error) {
|
|||||||
TotpSecret: userSplit[2],
|
TotpSecret: userSplit[2],
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse secret file
|
||||||
|
func ParseSecretFile(contents string) string {
|
||||||
|
// Split to lines
|
||||||
|
lines := strings.Split(contents, "\n")
|
||||||
|
|
||||||
|
// Loop through the lines
|
||||||
|
for _, line := range lines {
|
||||||
|
// Check if the line is empty
|
||||||
|
if strings.TrimSpace(line) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the line
|
||||||
|
return strings.TrimSpace(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return an empty string
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a string matches a regex or a whitelist
|
||||||
|
func CheckWhitelist(whitelist string, str string) bool {
|
||||||
|
// Check if the whitelist is empty
|
||||||
|
if len(strings.TrimSpace(whitelist)) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the whitelist is a regex
|
||||||
|
if strings.HasPrefix(whitelist, "/") && strings.HasSuffix(whitelist, "/") {
|
||||||
|
// Create regex
|
||||||
|
re, err := regexp.Compile(whitelist[1 : len(whitelist)-1])
|
||||||
|
|
||||||
|
// Check if there was an error
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Error compiling regex")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the string matches the regex
|
||||||
|
if re.MatchString(str) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split the whitelist by comma
|
||||||
|
whitelistSplit := strings.Split(whitelist, ",")
|
||||||
|
|
||||||
|
// Loop through the whitelist
|
||||||
|
for _, item := range whitelistSplit {
|
||||||
|
// Check if the item matches with the string
|
||||||
|
if strings.TrimSpace(item) == str {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return false if no match was found
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capitalize just the first letter of a string
|
||||||
|
func Capitalize(str string) string {
|
||||||
|
if len(str) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.ToUpper(string([]rune(str)[0])) + string([]rune(str)[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize header removes all control characters from a string
|
||||||
|
func SanitizeHeader(header string) string {
|
||||||
|
return strings.Map(func(r rune) rune {
|
||||||
|
// Allow only printable ASCII characters (32-126) and safe whitespace (space, tab)
|
||||||
|
if r == ' ' || r == '\t' || (r >= 32 && r <= 126) {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}, header)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package utils_test
|
package utils_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -123,7 +124,7 @@ func TestGetSecret(t *testing.T) {
|
|||||||
expected := "test"
|
expected := "test"
|
||||||
|
|
||||||
// Create file
|
// Create file
|
||||||
err := os.WriteFile(file, []byte(expected), 0644)
|
err := os.WriteFile(file, []byte(fmt.Sprintf("\n\n \n\n\n %s \n\n \n ", expected)), 0644)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -285,15 +286,19 @@ func TestGetTinyauthLabels(t *testing.T) {
|
|||||||
// Test the get tinyauth labels function with a valid map
|
// Test the get tinyauth labels function with a valid map
|
||||||
labels := map[string]string{
|
labels := map[string]string{
|
||||||
"tinyauth.users": "user1,user2",
|
"tinyauth.users": "user1,user2",
|
||||||
"tinyauth.oauth.whitelist": "user1,user2",
|
"tinyauth.oauth.whitelist": "/regex/",
|
||||||
"tinyauth.allowed": "random",
|
"tinyauth.allowed": "random",
|
||||||
"random": "random",
|
"random": "random",
|
||||||
|
"tinyauth.headers": "X-Header=value",
|
||||||
}
|
}
|
||||||
|
|
||||||
expected := types.TinyauthLabels{
|
expected := types.TinyauthLabels{
|
||||||
Users: []string{"user1", "user2"},
|
Users: "user1,user2",
|
||||||
OAuthWhitelist: []string{"user1", "user2"},
|
OAuthWhitelist: "/regex/",
|
||||||
Allowed: "random",
|
Allowed: "random",
|
||||||
|
Headers: map[string]string{
|
||||||
|
"X-Header": "value",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
result := utils.GetTinyauthLabels(labels)
|
result := utils.GetTinyauthLabels(labels)
|
||||||
@@ -384,3 +389,143 @@ func TestParseUser(t *testing.T) {
|
|||||||
t.Fatalf("Expected error parsing user")
|
t.Fatalf("Expected error parsing user")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test the whitelist function
|
||||||
|
func TestCheckWhitelist(t *testing.T) {
|
||||||
|
t.Log("Testing check whitelist with a comma whitelist")
|
||||||
|
|
||||||
|
// Create variables
|
||||||
|
whitelist := "user1,user2,user3"
|
||||||
|
str := "user1"
|
||||||
|
expected := true
|
||||||
|
|
||||||
|
// Test the check whitelist function
|
||||||
|
result := utils.CheckWhitelist(whitelist, str)
|
||||||
|
|
||||||
|
// Check if the result is equal to the expected
|
||||||
|
if result != expected {
|
||||||
|
t.Fatalf("Expected %v, got %v", expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Testing check whitelist with a regex whitelist")
|
||||||
|
|
||||||
|
// Create variables
|
||||||
|
whitelist = "/^user[0-9]+$/"
|
||||||
|
str = "user1"
|
||||||
|
expected = true
|
||||||
|
|
||||||
|
// Test the check whitelist function
|
||||||
|
result = utils.CheckWhitelist(whitelist, str)
|
||||||
|
|
||||||
|
// Check if the result is equal to the expected
|
||||||
|
if result != expected {
|
||||||
|
t.Fatalf("Expected %v, got %v", expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Testing check whitelist with an empty whitelist")
|
||||||
|
|
||||||
|
// Create variables
|
||||||
|
whitelist = ""
|
||||||
|
str = "user1"
|
||||||
|
expected = true
|
||||||
|
|
||||||
|
// Test the check whitelist function
|
||||||
|
result = utils.CheckWhitelist(whitelist, str)
|
||||||
|
|
||||||
|
// Check if the result is equal to the expected
|
||||||
|
if result != expected {
|
||||||
|
t.Fatalf("Expected %v, got %v", expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Testing check whitelist with an invalid regex whitelist")
|
||||||
|
|
||||||
|
// Create variables
|
||||||
|
whitelist = "/^user[0-9+$/"
|
||||||
|
str = "user1"
|
||||||
|
expected = false
|
||||||
|
|
||||||
|
// Test the check whitelist function
|
||||||
|
result = utils.CheckWhitelist(whitelist, str)
|
||||||
|
|
||||||
|
// Check if the result is equal to the expected
|
||||||
|
if result != expected {
|
||||||
|
t.Fatalf("Expected %v, got %v", expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Testing check whitelist with a non matching whitelist")
|
||||||
|
|
||||||
|
// Create variables
|
||||||
|
whitelist = "user1,user2,user3"
|
||||||
|
str = "user4"
|
||||||
|
expected = false
|
||||||
|
|
||||||
|
// Test the check whitelist function
|
||||||
|
result = utils.CheckWhitelist(whitelist, str)
|
||||||
|
|
||||||
|
// Check if the result is equal to the expected
|
||||||
|
if result != expected {
|
||||||
|
t.Fatalf("Expected %v, got %v", expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test capitalize
|
||||||
|
func TestCapitalize(t *testing.T) {
|
||||||
|
t.Log("Testing capitalize with a valid string")
|
||||||
|
|
||||||
|
// Create variables
|
||||||
|
str := "test"
|
||||||
|
expected := "Test"
|
||||||
|
|
||||||
|
// Test the capitalize function
|
||||||
|
result := utils.Capitalize(str)
|
||||||
|
|
||||||
|
// Check if the result is equal to the expected
|
||||||
|
if result != expected {
|
||||||
|
t.Fatalf("Expected %v, got %v", expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Testing capitalize with an empty string")
|
||||||
|
|
||||||
|
// Create variables
|
||||||
|
str = ""
|
||||||
|
expected = ""
|
||||||
|
|
||||||
|
// Test the capitalize function
|
||||||
|
result = utils.Capitalize(str)
|
||||||
|
|
||||||
|
// Check if the result is equal to the expected
|
||||||
|
if result != expected {
|
||||||
|
t.Fatalf("Expected %v, got %v", expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the header sanitizer
|
||||||
|
func TestSanitizeHeader(t *testing.T) {
|
||||||
|
t.Log("Testing sanitize header with a valid string")
|
||||||
|
|
||||||
|
// Create variables
|
||||||
|
str := "X-Header=value"
|
||||||
|
expected := "X-Header=value"
|
||||||
|
|
||||||
|
// Test the sanitize header function
|
||||||
|
result := utils.SanitizeHeader(str)
|
||||||
|
|
||||||
|
// Check if the result is equal to the expected
|
||||||
|
if result != expected {
|
||||||
|
t.Fatalf("Expected %v, got %v", expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Testing sanitize header with an invalid string")
|
||||||
|
|
||||||
|
// Create variables
|
||||||
|
str = "X-Header=val\nue"
|
||||||
|
expected = "X-Header=value"
|
||||||
|
|
||||||
|
// Test the sanitize header function
|
||||||
|
result = utils.SanitizeHeader(str)
|
||||||
|
|
||||||
|
// Check if the result is equal to the expected
|
||||||
|
if result != expected {
|
||||||
|
t.Fatalf("Expected %v, got %v", expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user