Compare commits

..
24 Commits
Author SHA1 Message Date
Stavros 9eabfe89fd chore: update screenshot 2026-07-15 16:48:16 +03:00
StavrosandGitHub dade1e2c8f refactor: rework rate limit logic (#1008) 2026-07-15 15:30:57 +03:00
StavrosandGitHub 6e095fd4f2 refactor: rework the way trusted proxies ip work (#1007) 2026-07-15 15:17:55 +03:00
Stavros 2a0d7bd37a chore: update sponsors section in readme 2026-07-15 01:59:19 +03:00
StavrosandGitHub c22925c2fb fix: use constant time in user checks (#1004) 2026-07-14 16:49:59 +03:00
StavrosandGitHub d946926c36 feat: allow existing query params in oidc redirect uri (#1003) 2026-07-14 16:36:57 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8881116360 chore(deps): bump softprops/action-gh-release from 3.0.1 to 3.0.2 (#1001)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-14 16:17:36 +03:00
StavrosandGitHub e75605b2c5 refactor: move domain check into small helper util (#1000) 2026-07-14 14:20:15 +03:00
Florian IlchandGitHub 79bcccbe43 feat: support proxy environment variables for OAuth requests (#999) 2026-07-14 13:18:28 +03:00
StavrosandGitHub b62bb2d37a refactor: generate mdx docs with tabs for each config option (#996) 2026-07-13 18:07:47 +03:00
Stavros 791adbb54d fix: remove omitempty suffix from docs gen 2026-07-11 17:18:35 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4cd4d67bda chore(deps): bump the minor-patch group in /frontend with 3 updates (#991)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-11 16:23:04 +03:00
Stavros 009ae30b47 Update Crowdin configuration file
[ci skip]
2026-07-11 13:05:07 +03:00
Stavros 11b534abc7 chore: add 1 day cooldown for dependabot updates 2026-07-11 00:25:17 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c575376215 chore(deps): bump the minor-patch group across 1 directory with 7 updates (#990)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-11 00:21:04 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4c23f55940 chore(deps): bump the minor-patch group across 1 directory with 2 updates (#983)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-10 17:48:57 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5a61958815 chore(deps): bump github/codeql-action/upload-sarif from 4.36.3 to 4.37.0 (#984)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-10 17:48:15 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e29550857c chore(deps): bump docker/login-action from 4.3.0 to 4.4.0 (#982)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-10 17:47:48 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
acad7f08e6 chore(deps): bump node from 26.4-alpine3.23 to 26.5-alpine3.23 (#981)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-10 17:46:43 +03:00
StavrosGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>CodeRabbit
405d91065e New Crowdin updates (#986)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
2026-07-09 20:16:17 +03:00
Stavros 82993624ba chore: remove dup i18n key 2026-07-09 18:40:37 +03:00
Stavros 0812bc3869 fix: truncate forgot password text if it's too big 2026-07-09 16:18:28 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6ff54f62fd chore(deps-dev): bump @types/node from 25.7.0 to 26.1.0 in /frontend (#971)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 14:16:14 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ff968ef08f chore(deps): bump react-router from 7.15.1 to 8.1.0 in /frontend (#970)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 14:14:52 +03:00
74 changed files with 4402 additions and 3111 deletions
+4 -4
View File
@@ -4,6 +4,10 @@
# The base URL where the app is hosted. # The base URL where the app is hosted.
TINYAUTH_APPURL= TINYAUTH_APPURL=
# Path to config file.
TINYAUTH_CONFIGFILE=
# Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment.
TINYAUTH_LABELPROVIDER="auto"
# database config # database config
@@ -97,8 +101,6 @@ TINYAUTH_AUTH_SESSIONMAXLIFETIME=0
TINYAUTH_AUTH_LOGINTIMEOUT=300 TINYAUTH_AUTH_LOGINTIMEOUT=300
# Maximum login retries. # Maximum login retries.
TINYAUTH_AUTH_LOGINMAXRETRIES=3 TINYAUTH_AUTH_LOGINMAXRETRIES=3
# Enable lockdown mode after maximum login retries. Lockdown mode limit is calculated automatically.
TINYAUTH_AUTH_LOCKDOWNENABLED=true
# Comma-separated list of trusted proxy addresses. # Comma-separated list of trusted proxy addresses.
TINYAUTH_AUTH_TRUSTEDPROXIES= TINYAUTH_AUTH_TRUSTEDPROXIES=
# ACL policy for allow-by-default or deny-by-default, available options are allow and deny, default is allow. # ACL policy for allow-by-default or deny-by-default, available options are allow and deny, default is allow.
@@ -233,8 +235,6 @@ TINYAUTH_TAILSCALE_APITOKENFILE=
TINYAUTH_TAILSCALE_TAILNET= TINYAUTH_TAILSCALE_TAILNET=
# Cache duration for Tailscale device and user lists in seconds. # Cache duration for Tailscale device and user lists in seconds.
TINYAUTH_TAILSCALE_CACHEDURATION=300 TINYAUTH_TAILSCALE_CACHEDURATION=300
# Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment.
TINYAUTH_LABELPROVIDER="auto"
# log config # log config
+8
View File
@@ -9,6 +9,8 @@ updates:
- "minor" - "minor"
schedule: schedule:
interval: "daily" interval: "daily"
cooldown:
default-days: 1
- package-ecosystem: "gomod" - package-ecosystem: "gomod"
directory: "/" directory: "/"
@@ -19,13 +21,19 @@ updates:
- "minor" - "minor"
schedule: schedule:
interval: "daily" interval: "daily"
cooldown:
default-days: 1
- package-ecosystem: "docker" - package-ecosystem: "docker"
directory: "/" directory: "/"
schedule: schedule:
interval: "daily" interval: "daily"
cooldown:
default-days: 1
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
directory: "/" directory: "/"
schedule: schedule:
interval: "daily" interval: "daily"
cooldown:
default-days: 1
+8 -8
View File
@@ -23,7 +23,7 @@ jobs:
REPO: ${{ github.event.repository.name }} REPO: ${{ github.event.repository.name }}
- name: Create release - name: Create release
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3 uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3
with: with:
prerelease: true prerelease: true
tag_name: nightly tag_name: nightly
@@ -167,7 +167,7 @@ jobs:
images: ghcr.io/${{ github.repository_owner }}/tinyauth images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4 uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -226,7 +226,7 @@ jobs:
images: ghcr.io/${{ github.repository_owner }}/tinyauth images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4 uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -285,7 +285,7 @@ jobs:
images: ghcr.io/${{ github.repository_owner }}/tinyauth images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4 uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -344,7 +344,7 @@ jobs:
images: ghcr.io/${{ github.repository_owner }}/tinyauth images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4 uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -399,7 +399,7 @@ jobs:
merge-multiple: true merge-multiple: true
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4 uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -438,7 +438,7 @@ jobs:
merge-multiple: true merge-multiple: true
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4 uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -476,7 +476,7 @@ jobs:
merge-multiple: true merge-multiple: true
- name: Release - name: Release
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3 uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3
with: with:
files: binaries/* files: binaries/*
tag_name: nightly tag_name: nightly
+7 -7
View File
@@ -139,7 +139,7 @@ jobs:
images: ghcr.io/${{ github.repository_owner }}/tinyauth images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4 uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -196,7 +196,7 @@ jobs:
images: ghcr.io/${{ github.repository_owner }}/tinyauth images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4 uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -253,7 +253,7 @@ jobs:
images: ghcr.io/${{ github.repository_owner }}/tinyauth images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4 uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -310,7 +310,7 @@ jobs:
images: ghcr.io/${{ github.repository_owner }}/tinyauth images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4 uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -366,7 +366,7 @@ jobs:
merge-multiple: true merge-multiple: true
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4 uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -407,7 +407,7 @@ jobs:
merge-multiple: true merge-multiple: true
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4 uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -449,6 +449,6 @@ jobs:
merge-multiple: true merge-multiple: true
- name: Release - name: Release
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3 uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3
with: with:
files: binaries/* files: binaries/*
+1 -1
View File
@@ -38,6 +38,6 @@ jobs:
retention-days: 5 retention-days: 5
- name: Upload to code-scanning - name: Upload to code-scanning
uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4 uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
with: with:
sarif_file: results.sarif sarif_file: results.sarif
+2 -1
View File
@@ -1,5 +1,5 @@
# Site builder # Site builder
FROM node:26.4-alpine3.23 AS frontend-builder FROM node:26.5-alpine3.23 AS frontend-builder
WORKDIR /frontend WORKDIR /frontend
@@ -39,6 +39,7 @@ RUN go mod download
COPY ./cmd ./cmd COPY ./cmd ./cmd
COPY ./internal ./internal COPY ./internal ./internal
COPY ./pkg ./pkg
COPY --from=frontend-builder /frontend/dist ./internal/assets/dist COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
RUN CGO_ENABLED=0 go build -tags "${BUILD_TAGS}" -ldflags "${LDFLAGS} \ RUN CGO_ENABLED=0 go build -tags "${BUILD_TAGS}" -ldflags "${LDFLAGS} \
+1
View File
@@ -12,6 +12,7 @@ RUN go install github.com/go-delve/delve/cmd/dlv@v1.26.3
COPY ./cmd ./cmd COPY ./cmd ./cmd
COPY ./internal ./internal COPY ./internal ./internal
COPY ./pkg ./pkg
COPY ./air.toml ./ COPY ./air.toml ./
EXPOSE 3000 EXPOSE 3000
+2 -1
View File
@@ -1,5 +1,5 @@
# Site builder # Site builder
FROM node:26.4-alpine3.23 AS frontend-builder FROM node:26.5-alpine3.23 AS frontend-builder
WORKDIR /frontend WORKDIR /frontend
@@ -39,6 +39,7 @@ RUN go mod download
COPY ./cmd ./cmd/ COPY ./cmd ./cmd/
COPY ./internal ./internal COPY ./internal ./internal
COPY ./pkg ./pkg
COPY --from=frontend-builder /frontend/dist ./internal/assets/dist COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
RUN CGO_ENABLED=0 go build -tags "${BUILD_TAGS}" -ldflags "${LDFLAGS} \ RUN CGO_ENABLED=0 go build -tags "${BUILD_TAGS}" -ldflags "${LDFLAGS} \
+11 -11
View File
@@ -62,18 +62,19 @@ If you like, you can help translate Tinyauth into more languages by visiting the
Tinyauth is licensed under the GNU Affero General Public License v3.0. TL;DR — You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) AGPL-licensed code must also be made available under the AGPL along with build & install instructions. If you run a modified version over a network, you must also make the source available to the users of that service. For more information about the license check the [license](LICENSE) file. Tinyauth is licensed under the GNU Affero General Public License v3.0. TL;DR — You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) AGPL-licensed code must also be made available under the AGPL along with build & install instructions. If you run a modified version over a network, you must also make the source available to the users of that service. For more information about the license check the [license](LICENSE) file.
## Hosting Partners
If you use one of our partners, you can help support us while getting a great hosting deal.
<div>
<a title="InstaPods" target="_blank" href="https://app.instapods.com/dashboard/pods/create?app=tinyauth&ref=tinyauth"><img src="https://instapods.com/deploy-button.svg"></a>
</div>
## Sponsors ## Sponsors
A big thank you to the following people for providing me with more coffee: A big thank-you to the following individuals and companies
for their support for Tinyauth.
### Companies and Organizations
- [JetBrains for Open-Source](https://jb.gg/OpenSource)
- [CodeRabbit AI](https://www.coderabbit.ai)
- [Dosu](https://dosu.dev/)
- [InstaPods](https://instapods.com) - [Deploy Tinyauth from 3$/month](https://app.instapods.com/dashboard/pods/create?app=tinyauth&ref=tinyauth)
### Individuals
<!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https:&#x2F;&#x2F;github.com&#x2F;erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a>&nbsp;&nbsp;<a href="https://github.com/nicotsx"><img src="https:&#x2F;&#x2F;github.com&#x2F;nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a>&nbsp;&nbsp;<a href="https://github.com/SimpleHomelab"><img src="https:&#x2F;&#x2F;github.com&#x2F;SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a>&nbsp;&nbsp;<a href="https://github.com/jmadden91"><img src="https:&#x2F;&#x2F;github.com&#x2F;jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a>&nbsp;&nbsp;<a href="https://github.com/tribor"><img src="https:&#x2F;&#x2F;github.com&#x2F;tribor.png" width="64px" alt="User avatar: tribor" /></a>&nbsp;&nbsp;<a href="https://github.com/eliasbenb"><img src="https:&#x2F;&#x2F;github.com&#x2F;eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a>&nbsp;&nbsp;<a href="https://github.com/afunworm"><img src="https:&#x2F;&#x2F;github.com&#x2F;afunworm.png" width="64px" alt="User avatar: afunworm" /></a>&nbsp;&nbsp;<a href="https://github.com/chip-well"><img src="https:&#x2F;&#x2F;github.com&#x2F;chip-well.png" width="64px" alt="User avatar: chip-well" /></a>&nbsp;&nbsp;<a href="https://github.com/Lancelot-Enguerrand"><img src="https:&#x2F;&#x2F;github.com&#x2F;Lancelot-Enguerrand.png" width="64px" alt="User avatar: Lancelot-Enguerrand" /></a>&nbsp;&nbsp;<a href="https://github.com/allgoewer"><img src="https:&#x2F;&#x2F;github.com&#x2F;allgoewer.png" width="64px" alt="User avatar: allgoewer" /></a>&nbsp;&nbsp;<a href="https://github.com/NEANC"><img src="https:&#x2F;&#x2F;github.com&#x2F;NEANC.png" width="64px" alt="User avatar: NEANC" /></a>&nbsp;&nbsp;<a href="https://github.com/axjab"><img src="https:&#x2F;&#x2F;github.com&#x2F;axjab.png" width="64px" alt="User avatar: axjab" /></a>&nbsp;&nbsp;<a href="https://github.com/stegratech"><img src="https:&#x2F;&#x2F;github.com&#x2F;stegratech.png" width="64px" alt="User avatar: stegratech" /></a>&nbsp;&nbsp;<a href="https://github.com/apearson"><img src="https:&#x2F;&#x2F;github.com&#x2F;apearson.png" width="64px" alt="User avatar: apearson" /></a>&nbsp;&nbsp;<a href="https://github.com/Micky5991"><img src="https:&#x2F;&#x2F;github.com&#x2F;Micky5991.png" width="64px" alt="User avatar: Micky5991" /></a>&nbsp;&nbsp;<!-- sponsors --> <!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https:&#x2F;&#x2F;github.com&#x2F;erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a>&nbsp;&nbsp;<a href="https://github.com/nicotsx"><img src="https:&#x2F;&#x2F;github.com&#x2F;nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a>&nbsp;&nbsp;<a href="https://github.com/SimpleHomelab"><img src="https:&#x2F;&#x2F;github.com&#x2F;SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a>&nbsp;&nbsp;<a href="https://github.com/jmadden91"><img src="https:&#x2F;&#x2F;github.com&#x2F;jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a>&nbsp;&nbsp;<a href="https://github.com/tribor"><img src="https:&#x2F;&#x2F;github.com&#x2F;tribor.png" width="64px" alt="User avatar: tribor" /></a>&nbsp;&nbsp;<a href="https://github.com/eliasbenb"><img src="https:&#x2F;&#x2F;github.com&#x2F;eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a>&nbsp;&nbsp;<a href="https://github.com/afunworm"><img src="https:&#x2F;&#x2F;github.com&#x2F;afunworm.png" width="64px" alt="User avatar: afunworm" /></a>&nbsp;&nbsp;<a href="https://github.com/chip-well"><img src="https:&#x2F;&#x2F;github.com&#x2F;chip-well.png" width="64px" alt="User avatar: chip-well" /></a>&nbsp;&nbsp;<a href="https://github.com/Lancelot-Enguerrand"><img src="https:&#x2F;&#x2F;github.com&#x2F;Lancelot-Enguerrand.png" width="64px" alt="User avatar: Lancelot-Enguerrand" /></a>&nbsp;&nbsp;<a href="https://github.com/allgoewer"><img src="https:&#x2F;&#x2F;github.com&#x2F;allgoewer.png" width="64px" alt="User avatar: allgoewer" /></a>&nbsp;&nbsp;<a href="https://github.com/NEANC"><img src="https:&#x2F;&#x2F;github.com&#x2F;NEANC.png" width="64px" alt="User avatar: NEANC" /></a>&nbsp;&nbsp;<a href="https://github.com/axjab"><img src="https:&#x2F;&#x2F;github.com&#x2F;axjab.png" width="64px" alt="User avatar: axjab" /></a>&nbsp;&nbsp;<a href="https://github.com/stegratech"><img src="https:&#x2F;&#x2F;github.com&#x2F;stegratech.png" width="64px" alt="User avatar: stegratech" /></a>&nbsp;&nbsp;<a href="https://github.com/apearson"><img src="https:&#x2F;&#x2F;github.com&#x2F;apearson.png" width="64px" alt="User avatar: apearson" /></a>&nbsp;&nbsp;<a href="https://github.com/Micky5991"><img src="https:&#x2F;&#x2F;github.com&#x2F;Micky5991.png" width="64px" alt="User avatar: Micky5991" /></a>&nbsp;&nbsp;<!-- sponsors -->
@@ -81,7 +82,6 @@ A big thank you to the following people for providing me with more coffee:
- **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.
- **Coderabbit AI** for providing free AI code reviews.
- **Syrhu** for providing the background image of the app. - **Syrhu** for providing the background image of the app.
## Star History ## Star History
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 MiB

After

Width:  |  Height:  |  Size: 3.7 MiB

+5 -11
View File
@@ -1,12 +1,6 @@
"base_path": "." base_path: .
"base_url": "https://api.crowdin.com" base_url: 'https://api.crowdin.com'
preserve_hierarchy: 1
"preserve_hierarchy": true
files: files:
[ - source: /frontend/src/lib/i18n/locales/en.json
{ translation: /frontend/src/lib/i18n/locales/%locale%.json
"source": "/frontend/src/lib/i18n/locales/en.json",
"translation": "/frontend/src/lib/i18n/locales/%locale%.json",
},
]
+10 -10
View File
@@ -13,9 +13,9 @@
"packageManager": "pnpm@11.1.2", "packageManager": "pnpm@11.1.2",
"dependencies": { "dependencies": {
"@hookform/resolvers": "^5.4.0", "@hookform/resolvers": "^5.4.0",
"@radix-ui/react-dropdown-menu": "^2.1.19", "@radix-ui/react-dropdown-menu": "^2.1.20",
"@radix-ui/react-label": "^2.1.11", "@radix-ui/react-label": "^2.1.11",
"@radix-ui/react-select": "^2.3.2", "@radix-ui/react-select": "^2.3.3",
"@radix-ui/react-separator": "^1.1.11", "@radix-ui/react-separator": "^1.1.11",
"@radix-ui/react-slot": "^1.3.0", "@radix-ui/react-slot": "^1.3.0",
"@tailwindcss/vite": "^4.3.2", "@tailwindcss/vite": "^4.3.2",
@@ -23,18 +23,18 @@
"axios": "^1.18.1", "axios": "^1.18.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"i18next": "^26.3.4", "i18next": "^26.3.6",
"i18next-browser-languagedetector": "^8.2.1", "i18next-browser-languagedetector": "^8.2.1",
"i18next-resources-to-backend": "^1.2.1", "i18next-resources-to-backend": "^1.2.1",
"lucide-react": "^1.23.0", "lucide-react": "^1.24.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"radix-ui": "^1.6.1", "radix-ui": "^1.6.2",
"react": "^19.2.7", "react": "^19.2.7",
"react-dom": "^19.2.7", "react-dom": "^19.2.7",
"react-hook-form": "^7.81.0", "react-hook-form": "^7.81.0",
"react-i18next": "^17.0.8", "react-i18next": "^17.0.9",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-router": "^7.14.0", "react-router": "^8.2.0",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"tailwindcss": "^4.3.2", "tailwindcss": "^4.3.2",
@@ -43,7 +43,7 @@
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@tanstack/eslint-plugin-query": "^5.101.2", "@tanstack/eslint-plugin-query": "^5.101.2",
"@types/node": "^25.6.0", "@types/node": "^26.1.1",
"@types/react": "^19.2.17", "@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.3", "@vitejs/plugin-react": "^6.0.3",
@@ -54,7 +54,7 @@
"rollup-plugin-visualizer": "^7.0.1", "rollup-plugin-visualizer": "^7.0.1",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "~6.0.2", "typescript": "~6.0.2",
"typescript-eslint": "^8.62.1", "typescript-eslint": "^8.63.0",
"vite": "^8.1.3" "vite": "^8.1.4"
} }
} }
+507 -424
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -78,7 +78,7 @@ export const LoginForm = (props: Props) => {
`/forgot-password${props.params ? `${props.params}` : ""}`, `/forgot-password${props.params ? `${props.params}` : ""}`,
); );
}} }}
className="text-muted-foreground hover:text-muted-foreground/80 text-sm absolute right-0 bottom-[2.565rem]" // 2.565 is *just* perfect className="text-muted-foreground hover:text-muted-foreground/80 text-sm absolute right-0 bottom-[2.565rem] max-w-40 truncate" // 2.565 is *just* perfect
> >
{t("forgotPasswordTitle")} {t("forgotPasswordTitle")}
</a> </a>
+24 -2
View File
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "Authorized", "authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.", "authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.", "authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}", "authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
"openidScopeName": "OpenID Connect", "openidScopeName": "OpenID Connect",
"openidScopeDescription": "Allows the app to access your OpenID Connect information.", "openidScopeDescription": "Allows the app to access your OpenID Connect information.",
"emailScopeName": "Email", "emailScopeName": "Email",
@@ -79,5 +79,27 @@
"profileScopeName": "Profile", "profileScopeName": "Profile",
"profileScopeDescription": "Allows the app to access your profile information.", "profileScopeDescription": "Allows the app to access your profile information.",
"groupsScopeName": "Groups", "groupsScopeName": "Groups",
"groupsScopeDescription": "Allows the app to access your group information." "groupsScopeDescription": "Allows the app to access your group information.",
"backToLoginButton": "Back to login",
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
"quickActionsLanguage": "Language",
"quickActionsTheme": "Theme",
"quickActionsThemeLight": "Light",
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+24 -2
View File
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "Authorized", "authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.", "authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.", "authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}", "authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
"openidScopeName": "OpenID Connect", "openidScopeName": "OpenID Connect",
"openidScopeDescription": "Allows the app to access your OpenID Connect information.", "openidScopeDescription": "Allows the app to access your OpenID Connect information.",
"emailScopeName": "Email", "emailScopeName": "Email",
@@ -79,5 +79,27 @@
"profileScopeName": "Profile", "profileScopeName": "Profile",
"profileScopeDescription": "Allows the app to access your profile information.", "profileScopeDescription": "Allows the app to access your profile information.",
"groupsScopeName": "Groups", "groupsScopeName": "Groups",
"groupsScopeDescription": "Allows the app to access your group information." "groupsScopeDescription": "Allows the app to access your group information.",
"backToLoginButton": "Back to login",
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
"quickActionsLanguage": "Language",
"quickActionsTheme": "Theme",
"quickActionsThemeLight": "Light",
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+24 -2
View File
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "Authorized", "authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.", "authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.", "authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}", "authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
"openidScopeName": "OpenID Connect", "openidScopeName": "OpenID Connect",
"openidScopeDescription": "Allows the app to access your OpenID Connect information.", "openidScopeDescription": "Allows the app to access your OpenID Connect information.",
"emailScopeName": "Email", "emailScopeName": "Email",
@@ -79,5 +79,27 @@
"profileScopeName": "Profile", "profileScopeName": "Profile",
"profileScopeDescription": "Allows the app to access your profile information.", "profileScopeDescription": "Allows the app to access your profile information.",
"groupsScopeName": "Groups", "groupsScopeName": "Groups",
"groupsScopeDescription": "Allows the app to access your group information." "groupsScopeDescription": "Allows the app to access your group information.",
"backToLoginButton": "Back to login",
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
"quickActionsLanguage": "Language",
"quickActionsTheme": "Theme",
"quickActionsThemeLight": "Light",
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+24 -2
View File
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "Authorized", "authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.", "authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.", "authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}", "authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
"openidScopeName": "OpenID Connect", "openidScopeName": "OpenID Connect",
"openidScopeDescription": "Allows the app to access your OpenID Connect information.", "openidScopeDescription": "Allows the app to access your OpenID Connect information.",
"emailScopeName": "Email", "emailScopeName": "Email",
@@ -79,5 +79,27 @@
"profileScopeName": "Profile", "profileScopeName": "Profile",
"profileScopeDescription": "Allows the app to access your profile information.", "profileScopeDescription": "Allows the app to access your profile information.",
"groupsScopeName": "Groups", "groupsScopeName": "Groups",
"groupsScopeDescription": "Allows the app to access your group information." "groupsScopeDescription": "Allows the app to access your group information.",
"backToLoginButton": "Back to login",
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
"quickActionsLanguage": "Language",
"quickActionsTheme": "Theme",
"quickActionsThemeLight": "Light",
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+24 -2
View File
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "Authorized", "authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.", "authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.", "authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}", "authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
"openidScopeName": "OpenID Connect", "openidScopeName": "OpenID Connect",
"openidScopeDescription": "Allows the app to access your OpenID Connect information.", "openidScopeDescription": "Allows the app to access your OpenID Connect information.",
"emailScopeName": "Email", "emailScopeName": "Email",
@@ -79,5 +79,27 @@
"profileScopeName": "Profile", "profileScopeName": "Profile",
"profileScopeDescription": "Allows the app to access your profile information.", "profileScopeDescription": "Allows the app to access your profile information.",
"groupsScopeName": "Groups", "groupsScopeName": "Groups",
"groupsScopeDescription": "Allows the app to access your group information." "groupsScopeDescription": "Allows the app to access your group information.",
"backToLoginButton": "Back to login",
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
"quickActionsLanguage": "Language",
"quickActionsTheme": "Theme",
"quickActionsThemeLight": "Light",
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+24 -2
View File
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "Authorized", "authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.", "authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.", "authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}", "authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
"openidScopeName": "OpenID Connect", "openidScopeName": "OpenID Connect",
"openidScopeDescription": "Allows the app to access your OpenID Connect information.", "openidScopeDescription": "Allows the app to access your OpenID Connect information.",
"emailScopeName": "Email", "emailScopeName": "Email",
@@ -79,5 +79,27 @@
"profileScopeName": "Profile", "profileScopeName": "Profile",
"profileScopeDescription": "Allows the app to access your profile information.", "profileScopeDescription": "Allows the app to access your profile information.",
"groupsScopeName": "Groups", "groupsScopeName": "Groups",
"groupsScopeDescription": "Allows the app to access your group information." "groupsScopeDescription": "Ermöglicht der App, auf Ihre Benutzergruppen-Informationen zuzugreifen.",
"backToLoginButton": "Zurück zum Login",
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
"quickActionsLanguage": "Language",
"quickActionsTheme": "Theme",
"quickActionsThemeLight": "Light",
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+25 -3
View File
@@ -48,7 +48,7 @@
"unauthorizedIpSubtitle": "Η διεύθυνση IP σας <code>{{ip}}</code> δεν είναι εξουσιοδοτημένη να έχει πρόσβαση στον πόρο <code>{{resource}}</code>.", "unauthorizedIpSubtitle": "Η διεύθυνση IP σας <code>{{ip}}</code> δεν είναι εξουσιοδοτημένη να έχει πρόσβαση στον πόρο <code>{{resource}}</code>.",
"unauthorizedButton": "Προσπαθήστε ξανά", "unauthorizedButton": "Προσπαθήστε ξανά",
"cancelTitle": "Ακύρωση", "cancelTitle": "Ακύρωση",
"forgotPasswordTitle": "Ξεχάσατε το συνθηματικό σας;", "forgotPasswordTitle": "Ξεχάσατε;",
"failedToFetchProvidersTitle": "Αποτυχία φόρτωσης παρόχων πιστοποίησης. Παρακαλώ ελέγξτε τις ρυθμίσεις σας.", "failedToFetchProvidersTitle": "Αποτυχία φόρτωσης παρόχων πιστοποίησης. Παρακαλώ ελέγξτε τις ρυθμίσεις σας.",
"errorTitle": "Παρουσιάστηκε ένα σφάλμα", "errorTitle": "Παρουσιάστηκε ένα σφάλμα",
"errorSubtitleInfo": "Το ακόλουθο σφάλμα προέκυψε κατά την επεξεργασία του αιτήματός σας:", "errorSubtitleInfo": "Το ακόλουθο σφάλμα προέκυψε κατά την επεξεργασία του αιτήματός σας:",
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "Εξουσιοδοτημένος", "authorizeSuccessTitle": "Εξουσιοδοτημένος",
"authorizeSuccessSubtitle": "Θα μεταφερθείτε στην εφαρμογή σε λίγα δευτερόλεπτα.", "authorizeSuccessSubtitle": "Θα μεταφερθείτε στην εφαρμογή σε λίγα δευτερόλεπτα.",
"authorizeErrorClientInfo": "Παρουσιάστηκε σφάλμα κατά τη φόρτωση των πληροφοριών. Παρακαλώ προσπαθήστε ξανά αργότερα.", "authorizeErrorClientInfo": "Παρουσιάστηκε σφάλμα κατά τη φόρτωση των πληροφοριών. Παρακαλώ προσπαθήστε ξανά αργότερα.",
"authorizeErrorMissingParams": "Οι παρακάτω απαραίτητες πληροφορίες λείπουν από το αίτημά σας: {{missingParams}}", "authorizeErrorInvalidParams": "Το αίτημα δεν περιέχει τις απαιτούμενες παραμέτρους ή έχει μη έγκυρες παραμέτρους. Ελέγξτε τη διεύθυνση URL και προσπαθήστε ξανά.",
"openidScopeName": "Σύνδεση OpenID", "openidScopeName": "Σύνδεση OpenID",
"openidScopeDescription": "Επιτρέπει στην εφαρμογή την πρόσβαση στις πληροφορίες σύνδεσης OpenID.", "openidScopeDescription": "Επιτρέπει στην εφαρμογή την πρόσβαση στις πληροφορίες σύνδεσης OpenID.",
"emailScopeName": "Ηλεκτρονικό ταχυδρομείο", "emailScopeName": "Ηλεκτρονικό ταχυδρομείο",
@@ -79,5 +79,27 @@
"profileScopeName": "Προφίλ", "profileScopeName": "Προφίλ",
"profileScopeDescription": "Επιτρέπει στην εφαρμογή να έχει πρόσβαση στις πληροφορίες του προφίλ σας.", "profileScopeDescription": "Επιτρέπει στην εφαρμογή να έχει πρόσβαση στις πληροφορίες του προφίλ σας.",
"groupsScopeName": "Ομάδες", "groupsScopeName": "Ομάδες",
"groupsScopeDescription": "Επιτρέπει στην εφαρμογή την πρόσβαση στις πληροφορίες ομάδας σας." "groupsScopeDescription": "Επιτρέπει στην εφαρμογή την πρόσβαση στις πληροφορίες ομάδας σας.",
"backToLoginButton": "Επιστροφή στη σελίδα εισόδου",
"phoneScopeName": "Τηλέφωνο",
"phoneScopeDescription": "Επιτρέπει στην εφαρμογή να έχει πρόσβαση στον αριθμό τηλεφώνου σας.",
"addressScopeName": "Διεύθυνση",
"addressScopeDescription": "Επιτρέπει στην εφαρμογή να έχει πρόσβαση στη διεύθυνσή σας.",
"loginTailscaleTitle": "Συνέχεια με Tailscale",
"loginTailscaleDescription": "Φαίνεται ότι έχετε πρόσβαση στο Tinyauth από μια εξουσιοδοτημένη συσκευή Tailscale. Θα θέλατε να συνεχίσετε με τη σύνδεση Tailscale σας;",
"loginTailscaleDeviceName": "Όνομα συσκευής:",
"loginTailscaleOtherMethod": "Σύνδεση με άλλη μέθοδο",
"loginTailscaleSuccess": "Επιτυχής ταυτοποίηση με Tailscale.",
"loginTailscaleFail": "Αποτυχία ταυτοποίησης με Tailscale. Δοκιμάστε ξανά ή χρησιμοποιήστε μια άλλη μέθοδο σύνδεσης.",
"logoutTailscaleSubtitle": "Αυτή τη στιγμή είστε συνδεδεμένοι με Tailscale με τη συσκευή σας <code>{{deviceName}}</code>. Κάντε κλικ στο παρακάτω κουμπί για να αποσυνδεθείτε.",
"quickActionsLanguage": "Γλώσσα",
"quickActionsTheme": "Θέμα",
"quickActionsThemeLight": "Φωτεινό",
"quickActionsThemeDark": "Σκούρο",
"quickActionsThemeSystem": "Σύστημα",
"quickActionsLogout": "Αποσύνδεση",
"quickActionsTitle": "Γρήγορες ενέργειες",
"quickActionsProviderLocal": "Τοπικό",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
-1
View File
@@ -88,7 +88,6 @@
"loginTailscaleTitle": "Continue with Tailscale", "loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?", "loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:", "loginTailscaleDeviceName": "Device name:",
"loginTailscaleSubmit": "Continue with Tailscale",
"loginTailscaleOtherMethod": "Login with another method", "loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.", "loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.", "loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
-1
View File
@@ -88,7 +88,6 @@
"loginTailscaleTitle": "Continue with Tailscale", "loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?", "loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:", "loginTailscaleDeviceName": "Device name:",
"loginTailscaleSubmit": "Continue with Tailscale",
"loginTailscaleOtherMethod": "Login with another method", "loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.", "loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.", "loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
+24 -2
View File
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "Authorized", "authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.", "authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.", "authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}", "authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
"openidScopeName": "OpenID Connect", "openidScopeName": "OpenID Connect",
"openidScopeDescription": "Allows the app to access your OpenID Connect information.", "openidScopeDescription": "Allows the app to access your OpenID Connect information.",
"emailScopeName": "Email", "emailScopeName": "Email",
@@ -79,5 +79,27 @@
"profileScopeName": "Profile", "profileScopeName": "Profile",
"profileScopeDescription": "Allows the app to access your profile information.", "profileScopeDescription": "Allows the app to access your profile information.",
"groupsScopeName": "Groups", "groupsScopeName": "Groups",
"groupsScopeDescription": "Allows the app to access your group information." "groupsScopeDescription": "Allows the app to access your group information.",
"backToLoginButton": "Back to login",
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
"quickActionsLanguage": "Language",
"quickActionsTheme": "Theme",
"quickActionsThemeLight": "Light",
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+24 -2
View File
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "Authorized", "authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.", "authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.", "authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}", "authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
"openidScopeName": "OpenID Connect", "openidScopeName": "OpenID Connect",
"openidScopeDescription": "Allows the app to access your OpenID Connect information.", "openidScopeDescription": "Allows the app to access your OpenID Connect information.",
"emailScopeName": "Email", "emailScopeName": "Email",
@@ -79,5 +79,27 @@
"profileScopeName": "Profile", "profileScopeName": "Profile",
"profileScopeDescription": "Allows the app to access your profile information.", "profileScopeDescription": "Allows the app to access your profile information.",
"groupsScopeName": "Groups", "groupsScopeName": "Groups",
"groupsScopeDescription": "Allows the app to access your group information." "groupsScopeDescription": "Allows the app to access your group information.",
"backToLoginButton": "Back to login",
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
"quickActionsLanguage": "Language",
"quickActionsTheme": "Theme",
"quickActionsThemeLight": "Light",
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+24 -2
View File
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "Autorisé", "authorizeSuccessTitle": "Autorisé",
"authorizeSuccessSubtitle": "Vous allez être redirigé vers l'application dans quelques secondes.", "authorizeSuccessSubtitle": "Vous allez être redirigé vers l'application dans quelques secondes.",
"authorizeErrorClientInfo": "Une erreur est survenue lors du chargement des informations du client. Veuillez réessayer plus tard.", "authorizeErrorClientInfo": "Une erreur est survenue lors du chargement des informations du client. Veuillez réessayer plus tard.",
"authorizeErrorMissingParams": "Les paramètres suivants sont manquants : {{missingParams}}", "authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
"openidScopeName": "Connexion OpenID", "openidScopeName": "Connexion OpenID",
"openidScopeDescription": "Autorise l'application à accéder à vos informations \"OpenID Connect\".", "openidScopeDescription": "Autorise l'application à accéder à vos informations \"OpenID Connect\".",
"emailScopeName": "Email", "emailScopeName": "Email",
@@ -79,5 +79,27 @@
"profileScopeName": "Profil", "profileScopeName": "Profil",
"profileScopeDescription": "Autorise l'application à accéder aux informations de votre profil.", "profileScopeDescription": "Autorise l'application à accéder aux informations de votre profil.",
"groupsScopeName": "Groupes", "groupsScopeName": "Groupes",
"groupsScopeDescription": "Autorise une application à accéder aux informations de votre groupe." "groupsScopeDescription": "Autorise une application à accéder aux informations de votre groupe.",
"backToLoginButton": "Revenir à la page de connexion",
"phoneScopeName": "Téléphone",
"phoneScopeDescription": "Autorise l'application à accéder à votre numéro de téléphone.",
"addressScopeName": "Adresse",
"addressScopeDescription": "Autorise l'application d'accéder à votre adresse.",
"loginTailscaleTitle": "Continuer avec Tailscale",
"loginTailscaleDescription": "Vous semblez accéder à Tinyauth à partir d'un appareil Tailscale autorisé. Voulez-vous continuer avec votre connexion Tailscale ?",
"loginTailscaleDeviceName": "Nom du périphérique:",
"loginTailscaleOtherMethod": "Se connecter avec une autre méthode",
"loginTailscaleSuccess": "Authentifié avec Tailscale.",
"loginTailscaleFail": "Échec de l'authentification Tailscale. Veuillez réessayer ou utiliser une autre méthode de connexion.",
"logoutTailscaleSubtitle": "Vous êtes actuellement connecté en tant que <code>{{deviceName}}</code>, cliquez sur le bouton ci-dessous pour vous déconnecter.",
"quickActionsLanguage": "Language",
"quickActionsTheme": "Theme",
"quickActionsThemeLight": "Light",
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+24 -2
View File
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "Authorized", "authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.", "authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.", "authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}", "authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
"openidScopeName": "OpenID Connect", "openidScopeName": "OpenID Connect",
"openidScopeDescription": "Allows the app to access your OpenID Connect information.", "openidScopeDescription": "Allows the app to access your OpenID Connect information.",
"emailScopeName": "Email", "emailScopeName": "Email",
@@ -79,5 +79,27 @@
"profileScopeName": "Profile", "profileScopeName": "Profile",
"profileScopeDescription": "Allows the app to access your profile information.", "profileScopeDescription": "Allows the app to access your profile information.",
"groupsScopeName": "Groups", "groupsScopeName": "Groups",
"groupsScopeDescription": "Allows the app to access your group information." "groupsScopeDescription": "Allows the app to access your group information.",
"backToLoginButton": "Back to login",
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
"quickActionsLanguage": "Language",
"quickActionsTheme": "Theme",
"quickActionsThemeLight": "Light",
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+24 -2
View File
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "Authorized", "authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.", "authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.", "authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}", "authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
"openidScopeName": "OpenID Connect", "openidScopeName": "OpenID Connect",
"openidScopeDescription": "Allows the app to access your OpenID Connect information.", "openidScopeDescription": "Allows the app to access your OpenID Connect information.",
"emailScopeName": "Email", "emailScopeName": "Email",
@@ -79,5 +79,27 @@
"profileScopeName": "Profile", "profileScopeName": "Profile",
"profileScopeDescription": "Allows the app to access your profile information.", "profileScopeDescription": "Allows the app to access your profile information.",
"groupsScopeName": "Groups", "groupsScopeName": "Groups",
"groupsScopeDescription": "Allows the app to access your group information." "groupsScopeDescription": "Allows the app to access your group information.",
"backToLoginButton": "Back to login",
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
"quickActionsLanguage": "Language",
"quickActionsTheme": "Theme",
"quickActionsThemeLight": "Light",
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+24 -2
View File
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "Autorizzato", "authorizeSuccessTitle": "Autorizzato",
"authorizeSuccessSubtitle": "Verrai reindirizzato all'app in pochi secondi.", "authorizeSuccessSubtitle": "Verrai reindirizzato all'app in pochi secondi.",
"authorizeErrorClientInfo": "Si è verificato un errore durante il caricamento delle informazioni del client. Riprova.", "authorizeErrorClientInfo": "Si è verificato un errore durante il caricamento delle informazioni del client. Riprova.",
"authorizeErrorMissingParams": "I seguenti parametri sono mancanti: {{missingParams}}", "authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
"openidScopeName": "OpenID Connect", "openidScopeName": "OpenID Connect",
"openidScopeDescription": "Permetti all'app di accedere alle tue informazioni OpenID Connect.", "openidScopeDescription": "Permetti all'app di accedere alle tue informazioni OpenID Connect.",
"emailScopeName": "Email", "emailScopeName": "Email",
@@ -79,5 +79,27 @@
"profileScopeName": "Profilo", "profileScopeName": "Profilo",
"profileScopeDescription": "Consenti all'app di accedere alle informazioni del tuo profilo.", "profileScopeDescription": "Consenti all'app di accedere alle informazioni del tuo profilo.",
"groupsScopeName": "Gruppi", "groupsScopeName": "Gruppi",
"groupsScopeDescription": "Consenti all'app di accedere alle informazioni sui tuoi gruppi." "groupsScopeDescription": "Consenti all'app di accedere alle informazioni sui tuoi gruppi.",
"backToLoginButton": "Back to login",
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
"quickActionsLanguage": "Language",
"quickActionsTheme": "Theme",
"quickActionsThemeLight": "Light",
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+44 -22
View File
@@ -2,44 +2,44 @@
"loginTitle": "Welcome back, login with", "loginTitle": "Welcome back, login with",
"loginTitleSimple": "Welcome back, please login", "loginTitleSimple": "Welcome back, please login",
"loginDivider": "Or", "loginDivider": "Or",
"loginUsername": "Username", "loginUsername": "ユーザーネーム",
"loginPassword": "Password", "loginPassword": "パスワード",
"loginSubmit": "Login", "loginSubmit": "ログイン",
"loginFailTitle": "Failed to log in", "loginFailTitle": "ログイン出来ない",
"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", "loginFailRateLimit": "You failed to login too many times. Please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "ログイン済み",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "An error occurred", "loginOauthFailTitle": "An error occurred",
"loginOauthFailSubtitle": "Failed to get OAuth URL", "loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting", "loginOauthSuccessTitle": "リダイレクトしています",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect", "loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now", "loginOauthAutoRedirectButton": "今すぐリダイレクト",
"continueTitle": "Continue", "continueTitle": "続行",
"continueRedirectingTitle": "Redirecting...", "continueRedirectingTitle": "リダイレクトしています…",
"continueRedirectingSubtitle": "You should be redirected to the app soon", "continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectManually": "Redirect me manually", "continueRedirectManually": "Redirect me manually",
"continueInsecureRedirectTitle": "Insecure redirect", "continueInsecureRedirectTitle": "安全でないリダイレク",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueUntrustedRedirectTitle": "Untrusted redirect", "continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"logoutFailTitle": "Failed to log out", "logoutFailTitle": "ログアウト出来ません",
"logoutFailSubtitle": "Please try again", "logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out", "logoutSuccessTitle": "Logged out",
"logoutSuccessSubtitle": "You have been logged out", "logoutSuccessSubtitle": "ログアウトしました",
"logoutTitle": "Logout", "logoutTitle": "ログアウト",
"logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.", "logoutUsernameSubtitle": "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": "ページが見つかりません",
"notFoundSubtitle": "The page you are looking for does not exist.", "notFoundSubtitle": "The page you are looking for does not exist.",
"notFoundButton": "Go home", "notFoundButton": "ホームに戻る",
"totpFailTitle": "Failed to verify code", "totpFailTitle": "Failed to verify code",
"totpFailSubtitle": "Please check your code and try again", "totpFailSubtitle": "Please check your code and try again",
"totpSuccessTitle": "Verified", "totpSuccessTitle": "Verified",
"totpSuccessSubtitle": "Redirecting to your app", "totpSuccessSubtitle": "アプリにリダイレクトしています",
"totpTitle": "Enter your TOTP code", "totpTitle": "TOTPコード入れて",
"totpSubtitle": "Please enter the code from your authenticator app.", "totpSubtitle": "Please enter the code from your authenticator app.",
"unauthorizedTitle": "Unauthorized", "unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
@@ -47,8 +47,8 @@
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again", "unauthorizedButton": "Try again",
"cancelTitle": "Cancel", "cancelTitle": "キャンセル",
"forgotPasswordTitle": "Forgot your password?", "forgotPasswordTitle": "パスワード忘れた?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred", "errorTitle": "An error occurred",
"errorSubtitleInfo": "The following error occurred while processing your request:", "errorSubtitleInfo": "The following error occurred while processing your request:",
@@ -71,13 +71,35 @@
"authorizeSuccessTitle": "Authorized", "authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.", "authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.", "authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}", "authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
"openidScopeName": "OpenID Connect", "openidScopeName": "OpenID Connect",
"openidScopeDescription": "Allows the app to access your OpenID Connect information.", "openidScopeDescription": "Allows the app to access your OpenID Connect information.",
"emailScopeName": "Email", "emailScopeName": "Eメール",
"emailScopeDescription": "Allows the app to access your email address.", "emailScopeDescription": "Allows the app to access your email address.",
"profileScopeName": "Profile", "profileScopeName": "Profile",
"profileScopeDescription": "Allows the app to access your profile information.", "profileScopeDescription": "Allows the app to access your profile information.",
"groupsScopeName": "Groups", "groupsScopeName": "Groups",
"groupsScopeDescription": "Allows the app to access your group information." "groupsScopeDescription": "Allows the app to access your group information.",
"backToLoginButton": "Back to login",
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
"quickActionsLanguage": "Language",
"quickActionsTheme": "Theme",
"quickActionsThemeLight": "Light",
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+24 -2
View File
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "권한 부여 완료", "authorizeSuccessTitle": "권한 부여 완료",
"authorizeSuccessSubtitle": "몇 초 후에 앱으로 리디렉션됩니다.", "authorizeSuccessSubtitle": "몇 초 후에 앱으로 리디렉션됩니다.",
"authorizeErrorClientInfo": "클라이언트 정보를 불러오는 중 오류가 발생했습니다. 나중에 다시 시도해 주세요.", "authorizeErrorClientInfo": "클라이언트 정보를 불러오는 중 오류가 발생했습니다. 나중에 다시 시도해 주세요.",
"authorizeErrorMissingParams": "다음 매개변수가 누락되었습니다: {{missingParams}}", "authorizeErrorInvalidParams": "요청에 필수 매개변수가 누락되었거나 유효하지 않은 매개변수가 포함되어 있습니다. URL을 확인한 후 다시 시도해 주세요.",
"openidScopeName": "OpenID Connect", "openidScopeName": "OpenID Connect",
"openidScopeDescription": "앱이 OpenID Connect 정보에 접근할 수 있도록 허용합니다.", "openidScopeDescription": "앱이 OpenID Connect 정보에 접근할 수 있도록 허용합니다.",
"emailScopeName": "이메일", "emailScopeName": "이메일",
@@ -79,5 +79,27 @@
"profileScopeName": "프로필", "profileScopeName": "프로필",
"profileScopeDescription": "앱이 프로필 정보에 접근할 수 있도록 허용합니다.", "profileScopeDescription": "앱이 프로필 정보에 접근할 수 있도록 허용합니다.",
"groupsScopeName": "그룹", "groupsScopeName": "그룹",
"groupsScopeDescription": "앱이 그룹 정보에 접근할 수 있도록 허용합니다." "groupsScopeDescription": "앱이 그룹 정보에 접근할 수 있도록 허용합니다.",
"backToLoginButton": "로그인 페이지로 돌아가기",
"phoneScopeName": "전화번호",
"phoneScopeDescription": "앱이 전화번호에 접근할 수 있도록 허용합니다.",
"addressScopeName": "주소",
"addressScopeDescription": "앱이 주소에 접근할 수 있도록 허용합니다.",
"loginTailscaleTitle": "Tailscale로 계속",
"loginTailscaleDescription": "사용자님은 인증된 Tailscale 기기에서 Tinyauth에 접속 중인 것으로 보입니다. Tailscale 연결을 계속하시겠습니까?",
"loginTailscaleDeviceName": "기기 이름:",
"loginTailscaleOtherMethod": "다른 방법으로 로그인",
"loginTailscaleSuccess": "Tailscale을 통해 성공적으로 인증되었습니다.",
"loginTailscaleFail": "Tailscale을 통한 인증에 실패했습니다. 다른 방식을 사용해 주세요.",
"logoutTailscaleSubtitle": "현재 <code>{{deviceName}}</code>가 Tailscale을 통해 로그인 되어 있습니다. 로그아웃 하시려면 아래 버튼을 클릭하세요.",
"quickActionsLanguage": "언어",
"quickActionsTheme": "테마",
"quickActionsThemeLight": "밝은 테마",
"quickActionsThemeDark": "어두운 테마",
"quickActionsThemeSystem": "시스템",
"quickActionsLogout": "로그아웃",
"quickActionsTitle": "빠른 동작",
"quickActionsProviderLocal": "로컬",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+24 -2
View File
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "Geautoriseerd", "authorizeSuccessTitle": "Geautoriseerd",
"authorizeSuccessSubtitle": "Je wordt binnen enkele seconden doorgestuurd naar de app.", "authorizeSuccessSubtitle": "Je wordt binnen enkele seconden doorgestuurd naar de app.",
"authorizeErrorClientInfo": "Er is een fout opgetreden tijdens het laden van de cliëntinformatie. Probeer het later opnieuw.", "authorizeErrorClientInfo": "Er is een fout opgetreden tijdens het laden van de cliëntinformatie. Probeer het later opnieuw.",
"authorizeErrorMissingParams": "De volgende parameters ontbreken: {{missingParams}}", "authorizeErrorInvalidParams": "Er ontbreken verplichte parameters voor het verzoek of er zijn ongeldige parameters. Controleer de URL en probeer het opnieuw.",
"openidScopeName": "OpenID Connect", "openidScopeName": "OpenID Connect",
"openidScopeDescription": "Hiermee kan de app toegang krijgen tot jouw OpenID Connect-informatie.", "openidScopeDescription": "Hiermee kan de app toegang krijgen tot jouw OpenID Connect-informatie.",
"emailScopeName": "E-mail", "emailScopeName": "E-mail",
@@ -79,5 +79,27 @@
"profileScopeName": "Profiel", "profileScopeName": "Profiel",
"profileScopeDescription": "Hiermee kan de app toegang krijgen tot je profielinformatie.", "profileScopeDescription": "Hiermee kan de app toegang krijgen tot je profielinformatie.",
"groupsScopeName": "Groepen", "groupsScopeName": "Groepen",
"groupsScopeDescription": "Hiermee kan de app toegang krijgen tot jouw groepsinformatie." "groupsScopeDescription": "Hiermee kan de app toegang krijgen tot jouw groepsinformatie.",
"backToLoginButton": "Terug naar Inloggen",
"phoneScopeName": "Telefoon",
"phoneScopeDescription": "Geeft de app toegang tot je telefoonnummer.",
"addressScopeName": "Adres",
"addressScopeDescription": "Geeft de app toegang tot je adres.",
"loginTailscaleTitle": "Doorgaan met Tailscale",
"loginTailscaleDescription": "Je lijkt toegang te hebben tot Tinyauth vanaf een geautoriseerd Tailscale-apparaat. Wil je doorgaan met je Tailscale-verbinding?",
"loginTailscaleDeviceName": "Apparaatnaam:",
"loginTailscaleOtherMethod": "Op een andere manier inloggen",
"loginTailscaleSuccess": "Succesvol geauthenticeerd met Tailscale.",
"loginTailscaleFail": "Authenticatie met Tailscale mislukt. Probeer het opnieuw of gebruik een andere inlogmethode.",
"logoutTailscaleSubtitle": "Je bent momenteel ingelogd met Tailscale op je apparaat <code>{{deviceName}}</code>. Klik op de knop hieronder om uit te loggen.",
"quickActionsLanguage": "Taal",
"quickActionsTheme": "Thema",
"quickActionsThemeLight": "Licht",
"quickActionsThemeDark": "Donker",
"quickActionsThemeSystem": "Systeem",
"quickActionsLogout": "Uitloggen",
"quickActionsTitle": "Snelle acties",
"quickActionsProviderLocal": "Lokaal",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+24 -2
View File
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "Authorized", "authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.", "authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.", "authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}", "authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
"openidScopeName": "OpenID Connect", "openidScopeName": "OpenID Connect",
"openidScopeDescription": "Allows the app to access your OpenID Connect information.", "openidScopeDescription": "Allows the app to access your OpenID Connect information.",
"emailScopeName": "Email", "emailScopeName": "Email",
@@ -79,5 +79,27 @@
"profileScopeName": "Profile", "profileScopeName": "Profile",
"profileScopeDescription": "Allows the app to access your profile information.", "profileScopeDescription": "Allows the app to access your profile information.",
"groupsScopeName": "Groups", "groupsScopeName": "Groups",
"groupsScopeDescription": "Allows the app to access your group information." "groupsScopeDescription": "Allows the app to access your group information.",
"backToLoginButton": "Back to login",
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
"quickActionsLanguage": "Language",
"quickActionsTheme": "Theme",
"quickActionsThemeLight": "Light",
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+25 -3
View File
@@ -71,13 +71,35 @@
"authorizeSuccessTitle": "Autoryzowano", "authorizeSuccessTitle": "Autoryzowano",
"authorizeSuccessSubtitle": "Za kilka sekund nastąpi przekierowanie do aplikacji.", "authorizeSuccessSubtitle": "Za kilka sekund nastąpi przekierowanie do aplikacji.",
"authorizeErrorClientInfo": "Wystąpił błąd podczas ładowania informacji o kliencie. Spróbuj ponownie później.", "authorizeErrorClientInfo": "Wystąpił błąd podczas ładowania informacji o kliencie. Spróbuj ponownie później.",
"authorizeErrorMissingParams": "Brakuje następujących parametrów: {{missingParams}}", "authorizeErrorInvalidParams": "Żądanie nie zawiera wymaganych parametrów lub zawiera nieprawidłowe parametry. Sprawdź adres URL i spróbuj ponownie.",
"openidScopeName": "OpenID Connect", "openidScopeName": "OpenID Connect",
"openidScopeDescription": "Zezwala aplikacji na dostęp do informacji o OpenID Connect.", "openidScopeDescription": "Zezwala aplikacji na dostęp do informacji o OpenID Connect.",
"emailScopeName": "E-mail", "emailScopeName": "E-mail",
"emailScopeDescription": "Zezwala aplikacji na dostęp do adresów e-mail.", "emailScopeDescription": "Zezwala aplikacji na dostęp do adresów e-mail.",
"profileScopeName": "Profil", "profileScopeName": "Profil",
"profileScopeDescription": "Zezwala aplikacji na dostęp do informacji o porfilu.", "profileScopeDescription": "Zezwala aplikacji na dostęp do informacji o profilu.",
"groupsScopeName": "Grupy", "groupsScopeName": "Grupy",
"groupsScopeDescription": "Zezwala aplikacji na dostęp do informacji o grupie." "groupsScopeDescription": "Zezwala aplikacji na dostęp do informacji o grupie.",
"backToLoginButton": "Wróć do logowania",
"phoneScopeName": "Telefon",
"phoneScopeDescription": "Pozwala aplikacji na dostęp do Twojego numeru telefonu.",
"addressScopeName": "Adres",
"addressScopeDescription": "Pozwala aplikacji na dostęp do Twojego adresu.",
"loginTailscaleTitle": "Kontynuuj poprzez Tailscale",
"loginTailscaleDescription": "Wygląda na to, że uzyskujesz dostęp do Tinyauth z autoryzowanego urządzenia Tailscale. Czy chcesz kontynuować połączenie poprzez Tailscale?",
"loginTailscaleDeviceName": "Nazwa urządzenia:",
"loginTailscaleOtherMethod": "Zaloguj się inną metodą",
"loginTailscaleSuccess": "Pomyślnie uwierzytelniono poprzez Tailscale.",
"loginTailscaleFail": "Nie udało się uwierzytelnić poprzez Tailscale. Spróbuj ponownie lub użyj innej metody logowania.",
"logoutTailscaleSubtitle": "Jesteś obecnie zalogowany jako <code>{{deviceName}}</code>. Kliknij poniższy przycisk, aby się wylogować.",
"quickActionsLanguage": "Język",
"quickActionsTheme": "Motyw",
"quickActionsThemeLight": "Jasny",
"quickActionsThemeDark": "Ciemny",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Wyloguj się",
"quickActionsTitle": "Szybkie czynności",
"quickActionsProviderLocal": "Lokalny",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+24 -2
View File
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "Authorized", "authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.", "authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.", "authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}", "authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
"openidScopeName": "OpenID Connect", "openidScopeName": "OpenID Connect",
"openidScopeDescription": "Allows the app to access your OpenID Connect information.", "openidScopeDescription": "Allows the app to access your OpenID Connect information.",
"emailScopeName": "Email", "emailScopeName": "Email",
@@ -79,5 +79,27 @@
"profileScopeName": "Profile", "profileScopeName": "Profile",
"profileScopeDescription": "Allows the app to access your profile information.", "profileScopeDescription": "Allows the app to access your profile information.",
"groupsScopeName": "Groups", "groupsScopeName": "Groups",
"groupsScopeDescription": "Allows the app to access your group information." "groupsScopeDescription": "Allows the app to access your group information.",
"backToLoginButton": "Back to login",
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
"quickActionsLanguage": "Language",
"quickActionsTheme": "Theme",
"quickActionsThemeLight": "Light",
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+24 -2
View File
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "Authorized", "authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.", "authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.", "authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}", "authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
"openidScopeName": "OpenID Connect", "openidScopeName": "OpenID Connect",
"openidScopeDescription": "Allows the app to access your OpenID Connect information.", "openidScopeDescription": "Allows the app to access your OpenID Connect information.",
"emailScopeName": "Email", "emailScopeName": "Email",
@@ -79,5 +79,27 @@
"profileScopeName": "Profile", "profileScopeName": "Profile",
"profileScopeDescription": "Allows the app to access your profile information.", "profileScopeDescription": "Allows the app to access your profile information.",
"groupsScopeName": "Groups", "groupsScopeName": "Groups",
"groupsScopeDescription": "Allows the app to access your group information." "groupsScopeDescription": "Allows the app to access your group information.",
"backToLoginButton": "Back to login",
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
"quickActionsLanguage": "Language",
"quickActionsTheme": "Theme",
"quickActionsThemeLight": "Light",
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+24 -2
View File
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "Authorized", "authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.", "authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.", "authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}", "authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
"openidScopeName": "OpenID Connect", "openidScopeName": "OpenID Connect",
"openidScopeDescription": "Allows the app to access your OpenID Connect information.", "openidScopeDescription": "Allows the app to access your OpenID Connect information.",
"emailScopeName": "Email", "emailScopeName": "Email",
@@ -79,5 +79,27 @@
"profileScopeName": "Profile", "profileScopeName": "Profile",
"profileScopeDescription": "Allows the app to access your profile information.", "profileScopeDescription": "Allows the app to access your profile information.",
"groupsScopeName": "Groups", "groupsScopeName": "Groups",
"groupsScopeDescription": "Allows the app to access your group information." "groupsScopeDescription": "Allows the app to access your group information.",
"backToLoginButton": "Back to login",
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
"quickActionsLanguage": "Language",
"quickActionsTheme": "Theme",
"quickActionsThemeLight": "Light",
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+25 -3
View File
@@ -57,7 +57,7 @@
"fieldRequired": "Это поле является обязательным", "fieldRequired": "Это поле является обязательным",
"invalidInput": "Недопустимый ввод", "invalidInput": "Недопустимый ввод",
"domainWarningTitle": "Неверный домен", "domainWarningTitle": "Неверный домен",
"domainWarningSubtitle": "You are accessing this instance from an incorrect domain. If you proceed, you may encounter issues with authentication.", "domainWarningSubtitle": "Вы получаете доступ к этому экземпляру приложения через некорректный домен. Если вы продолжите, вы можете столкнуться с проблемами аутентификации.",
"domainWarningCurrent": "Текущий:", "domainWarningCurrent": "Текущий:",
"domainWarningExpected": "Ожидается:", "domainWarningExpected": "Ожидается:",
"ignoreTitle": "Игнорировать", "ignoreTitle": "Игнорировать",
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "Разрешено", "authorizeSuccessTitle": "Разрешено",
"authorizeSuccessSubtitle": "Вы будете перенаправлены в приложение через несколько секунд.", "authorizeSuccessSubtitle": "Вы будете перенаправлены в приложение через несколько секунд.",
"authorizeErrorClientInfo": "Произошла ошибка при загрузке информации о клиенте. Пожалуйста, повторите попытку позже.", "authorizeErrorClientInfo": "Произошла ошибка при загрузке информации о клиенте. Пожалуйста, повторите попытку позже.",
"authorizeErrorMissingParams": "Отсутствуют следующие параметры: {{missingParams}}", "authorizeErrorInvalidParams": "В запросе отсутствуют необходимые параметры или указаны недопустимые параметры. Проверьте URL-адрес и попробуйте снова.",
"openidScopeName": "Подключение OpenID", "openidScopeName": "Подключение OpenID",
"openidScopeDescription": "Приложение сможет получить доступ к информации подключённого OpenID.", "openidScopeDescription": "Приложение сможет получить доступ к информации подключённого OpenID.",
"emailScopeName": "Эл. Почта", "emailScopeName": "Эл. Почта",
@@ -79,5 +79,27 @@
"profileScopeName": "Профиль", "profileScopeName": "Профиль",
"profileScopeDescription": "Приложение сможет получить доступ к информации вашего профиля.", "profileScopeDescription": "Приложение сможет получить доступ к информации вашего профиля.",
"groupsScopeName": "Группы", "groupsScopeName": "Группы",
"groupsScopeDescription": "Приложение сможет получать доступ к информации о вашей группе." "groupsScopeDescription": "Приложение сможет получать доступ к информации о вашей группе.",
"backToLoginButton": "Вернуться к форме входа",
"phoneScopeName": "Телефон",
"phoneScopeDescription": "Приложение сможет получить доступ к вашему номеру телефона.",
"addressScopeName": "Адрес",
"addressScopeDescription": "Приложение сможет получить доступ к вашему адресу.",
"loginTailscaleTitle": "Продолжить через Tailscale",
"loginTailscaleDescription": "Похоже, вы используете авторизованное устройство Tailscale для доступа к Tinyauth. Хотите продолжить через подключение Tailscale?",
"loginTailscaleDeviceName": "Имя устройства:",
"loginTailscaleOtherMethod": "Войти другим способом",
"loginTailscaleSuccess": "Успешная авторизация через Tailscale.",
"loginTailscaleFail": "Не удалось авторизоваться через Tailscale. Пожалуйста, попробуйте ещё раз, или используйте другой способ.",
"logoutTailscaleSubtitle": "Вы вошли через Tailscale на устройстве <code>{{deviceName}}</code>. Нажмите кнопку ниже, чтобы выйти.",
"quickActionsLanguage": "Язык",
"quickActionsTheme": "Тема",
"quickActionsThemeLight": "Светлая",
"quickActionsThemeDark": "Тёмная",
"quickActionsThemeSystem": "Система",
"quickActionsLogout": "Выйти",
"quickActionsTitle": "Быстрые действия",
"quickActionsProviderLocal": "Локальный",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+24 -2
View File
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "Ауторизован", "authorizeSuccessTitle": "Ауторизован",
"authorizeSuccessSubtitle": "Бићете преусмерени на апликацију за неколико секунди.", "authorizeSuccessSubtitle": "Бићете преусмерени на апликацију за неколико секунди.",
"authorizeErrorClientInfo": "Појавила се грешка током учитавања информација о клијенту. Молим вас покушајте поново касније.", "authorizeErrorClientInfo": "Појавила се грешка током учитавања информација о клијенту. Молим вас покушајте поново касније.",
"authorizeErrorMissingParams": "Следећи параметри недостају: {{missingParams}}", "authorizeErrorInvalidParams": "Захтеву недостају неопходни параметри или су они неисправни. Молим вас проверите адресу и покушајте поново.",
"openidScopeName": "OpenID повезивање", "openidScopeName": "OpenID повезивање",
"openidScopeDescription": "Омогућава апликацији да приступа информацији о вашој OpenID вези.", "openidScopeDescription": "Омогућава апликацији да приступа информацији о вашој OpenID вези.",
"emailScopeName": "Е-пошта", "emailScopeName": "Е-пошта",
@@ -79,5 +79,27 @@
"profileScopeName": "Профил", "profileScopeName": "Профил",
"profileScopeDescription": "Омогућава апликацији да приступа информацијама о вашем профилу.", "profileScopeDescription": "Омогућава апликацији да приступа информацијама о вашем профилу.",
"groupsScopeName": "Групе", "groupsScopeName": "Групе",
"groupsScopeDescription": "Омогућава апликацији да приступа информацијама о вашој групи." "groupsScopeDescription": "Омогућава апликацији да приступа информацијама о вашој групи.",
"backToLoginButton": "Назад на пријаву",
"phoneScopeName": "Телефон",
"phoneScopeDescription": "Дозволите овој апликацији да приступи вашем броју телефона.",
"addressScopeName": "Адреса",
"addressScopeDescription": "Омогућава апликацији да приступи вашој адреси.",
"loginTailscaleTitle": "Настави са Tailscale-ом",
"loginTailscaleDescription": "Изгледа да приступате Tinyauth-у са овлашћеног Tailscale уређаја. Да ли желите да наставите са Tailscale везом?",
"loginTailscaleDeviceName": "Назив уређаја:",
"loginTailscaleOtherMethod": "Пријави се на други начин",
"loginTailscaleSuccess": "Успешно аутентификован са Tailscale-ом.",
"loginTailscaleFail": "Неуспела аутентификација са Tailscale-ом. Молим вас покушајте поново или користите други начин пријаве.",
"logoutTailscaleSubtitle": "Тренутно сте пријављени са Tailscale-ом на вашем уређају <code>{{deviceName}}</code>. Кликните на дугме испод за одјаву.",
"quickActionsLanguage": "Језик",
"quickActionsTheme": "Тема",
"quickActionsThemeLight": "Светло",
"quickActionsThemeDark": "Тамна",
"quickActionsThemeSystem": "Систем",
"quickActionsLogout": "Одјава",
"quickActionsTitle": "Брзе радње",
"quickActionsProviderLocal": "Локално",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+24 -2
View File
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "Authorized", "authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.", "authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.", "authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}", "authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
"openidScopeName": "OpenID Connect", "openidScopeName": "OpenID Connect",
"openidScopeDescription": "Allows the app to access your OpenID Connect information.", "openidScopeDescription": "Allows the app to access your OpenID Connect information.",
"emailScopeName": "Email", "emailScopeName": "Email",
@@ -79,5 +79,27 @@
"profileScopeName": "Profile", "profileScopeName": "Profile",
"profileScopeDescription": "Allows the app to access your profile information.", "profileScopeDescription": "Allows the app to access your profile information.",
"groupsScopeName": "Groups", "groupsScopeName": "Groups",
"groupsScopeDescription": "Allows the app to access your group information." "groupsScopeDescription": "Allows the app to access your group information.",
"backToLoginButton": "Back to login",
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
"quickActionsLanguage": "Language",
"quickActionsTheme": "Theme",
"quickActionsThemeLight": "Light",
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+24 -2
View File
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "Authorized", "authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.", "authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.", "authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}", "authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
"openidScopeName": "OpenID Connect", "openidScopeName": "OpenID Connect",
"openidScopeDescription": "Allows the app to access your OpenID Connect information.", "openidScopeDescription": "Allows the app to access your OpenID Connect information.",
"emailScopeName": "Email", "emailScopeName": "Email",
@@ -79,5 +79,27 @@
"profileScopeName": "Profile", "profileScopeName": "Profile",
"profileScopeDescription": "Allows the app to access your profile information.", "profileScopeDescription": "Allows the app to access your profile information.",
"groupsScopeName": "Groups", "groupsScopeName": "Groups",
"groupsScopeDescription": "Allows the app to access your group information." "groupsScopeDescription": "Allows the app to access your group information.",
"backToLoginButton": "Back to login",
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
"quickActionsLanguage": "Language",
"quickActionsTheme": "Theme",
"quickActionsThemeLight": "Light",
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+25 -3
View File
@@ -30,7 +30,7 @@
"logoutSuccessTitle": "Ви вийшли", "logoutSuccessTitle": "Ви вийшли",
"logoutSuccessSubtitle": "Ви вийшли з системи", "logoutSuccessSubtitle": "Ви вийшли з системи",
"logoutTitle": "Вийти", "logoutTitle": "Вийти",
"logoutUsernameSubtitle": "Зараз ви увійшли як <code>{{username}}</code>. Натисніть кнопку нижче для виходу.", "logoutUsernameSubtitle": "Ви увійшли як <code>{{username}}</code>. Натисніть кнопку нижче для виходу.",
"logoutOauthSubtitle": "Наразі ви увійшли як <code>{{username}}</code> використовуючи провайдера {{provider}} OAuth. Натисніть кнопку нижче, щоб вийти.", "logoutOauthSubtitle": "Наразі ви увійшли як <code>{{username}}</code> використовуючи провайдера {{provider}} OAuth. Натисніть кнопку нижче, щоб вийти.",
"notFoundTitle": "Сторінку не знайдено", "notFoundTitle": "Сторінку не знайдено",
"notFoundSubtitle": "Сторінка, яку ви шукаєте, не існує.", "notFoundSubtitle": "Сторінка, яку ви шукаєте, не існує.",
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "Авторизовано", "authorizeSuccessTitle": "Авторизовано",
"authorizeSuccessSubtitle": "Вас буде перенаправлено до програми за декілька секунд.", "authorizeSuccessSubtitle": "Вас буде перенаправлено до програми за декілька секунд.",
"authorizeErrorClientInfo": "Під час завантаження даних клієнта сталася помилка. Будь ласка, спробуйте ще раз пізніше.", "authorizeErrorClientInfo": "Під час завантаження даних клієнта сталася помилка. Будь ласка, спробуйте ще раз пізніше.",
"authorizeErrorMissingParams": "Відсутні наступні параметри: {{missingParams}}", "authorizeErrorInvalidParams": "У запиті відсутні обов'язкові або містяться некоректні параметри. Будь ласка, перевірте URL-адресу та спробуйте знову.",
"openidScopeName": "OpenID Connect", "openidScopeName": "OpenID Connect",
"openidScopeDescription": "Дозволяє програмі отримувати доступ до вашої інформації OpenID Connect.", "openidScopeDescription": "Дозволяє програмі отримувати доступ до вашої інформації OpenID Connect.",
"emailScopeName": "Електронна пошта", "emailScopeName": "Електронна пошта",
@@ -79,5 +79,27 @@
"profileScopeName": "Профіль", "profileScopeName": "Профіль",
"profileScopeDescription": "Дозволяє програмі отримувати доступ до інформації вашого профілю.", "profileScopeDescription": "Дозволяє програмі отримувати доступ до інформації вашого профілю.",
"groupsScopeName": "Групи", "groupsScopeName": "Групи",
"groupsScopeDescription": "Дозволяє програмі отримувати доступ до інформації про групу." "groupsScopeDescription": "Дозволяє програмі отримувати доступ до інформації про групу.",
"backToLoginButton": "Повернутися на сторінку входу",
"phoneScopeName": "Номер телефону",
"phoneScopeDescription": "Дозволяє програмі отримувати доступ до вашого номера телефону.",
"addressScopeName": "Адреса",
"addressScopeDescription": "Дозволяє програмі отримувати доступ до вашої адреси.",
"loginTailscaleTitle": "Продовжити з Tailscale",
"loginTailscaleDescription": "Здається, Ви отримали доступ до Tinyauth з авторизованого пристрою Tailscale. Продовжити з'єднання з Tailscale?",
"loginTailscaleDeviceName": "Назва пристрою:",
"loginTailscaleOtherMethod": "Увійти за допомогою іншого методу",
"loginTailscaleSuccess": "Успішно авторизовано з Tailscale.",
"loginTailscaleFail": "Не вдалося увійти з Tailscale. Будь ласка, спробуйте ще раз або скористайтеся іншим методом авторизації.",
"logoutTailscaleSubtitle": "Наразі ви ввійшли за допомогою Tailscale на пристрої <code>{{deviceName}}</code>. Натисніть на кнопку нижче, щоб вийти.",
"quickActionsLanguage": "Мова",
"quickActionsTheme": "Тема",
"quickActionsThemeLight": "Світла",
"quickActionsThemeDark": "Темна",
"quickActionsThemeSystem": "Система",
"quickActionsLogout": "Вийти",
"quickActionsTitle": "Швидкі дії",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+24 -2
View File
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "Authorized", "authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.", "authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.", "authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}", "authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
"openidScopeName": "OpenID Connect", "openidScopeName": "OpenID Connect",
"openidScopeDescription": "Allows the app to access your OpenID Connect information.", "openidScopeDescription": "Allows the app to access your OpenID Connect information.",
"emailScopeName": "Email", "emailScopeName": "Email",
@@ -79,5 +79,27 @@
"profileScopeName": "Profile", "profileScopeName": "Profile",
"profileScopeDescription": "Allows the app to access your profile information.", "profileScopeDescription": "Allows the app to access your profile information.",
"groupsScopeName": "Groups", "groupsScopeName": "Groups",
"groupsScopeDescription": "Allows the app to access your group information." "groupsScopeDescription": "Allows the app to access your group information.",
"backToLoginButton": "Back to login",
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
"quickActionsLanguage": "Language",
"quickActionsTheme": "Theme",
"quickActionsThemeLight": "Light",
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+24 -2
View File
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "已授权", "authorizeSuccessTitle": "已授权",
"authorizeSuccessSubtitle": "您将在几秒钟内被重定向到应用程序。", "authorizeSuccessSubtitle": "您将在几秒钟内被重定向到应用程序。",
"authorizeErrorClientInfo": "加载客户端信息时发生错误。请稍后再试。", "authorizeErrorClientInfo": "加载客户端信息时发生错误。请稍后再试。",
"authorizeErrorMissingParams": "参数缺失:{{missingParams}}", "authorizeErrorInvalidParams": "请求缺少所需参数或参数无效。请检查 URL 并重试。",
"openidScopeName": "OpenID 连接", "openidScopeName": "OpenID 连接",
"openidScopeDescription": "允许应用访问您的 OpenID 连接信息。", "openidScopeDescription": "允许应用访问您的 OpenID 连接信息。",
"emailScopeName": "邮箱", "emailScopeName": "邮箱",
@@ -79,5 +79,27 @@
"profileScopeName": "个人资料", "profileScopeName": "个人资料",
"profileScopeDescription": "允许应用访问您的个人信息。", "profileScopeDescription": "允许应用访问您的个人信息。",
"groupsScopeName": "分组", "groupsScopeName": "分组",
"groupsScopeDescription": "允许应用程序访问您的群组信息。" "groupsScopeDescription": "允许应用程序访问您的群组信息。",
"backToLoginButton": "返回登录界面",
"phoneScopeName": "手机",
"phoneScopeDescription": "允许此应用程序访问您的手机号码。",
"addressScopeName": "地址",
"addressScopeDescription": "允许此应用程序访问您的地址。",
"loginTailscaleTitle": "使用 Tailscale 继续",
"loginTailscaleDescription": "您似乎正在使用已授权的 Tailscale 设备访问 Tinyauth 。是否要继续使用您的 Tailscale 连接?",
"loginTailscaleDeviceName": "设备名称:",
"loginTailscaleOtherMethod": "使用其他方式登录",
"loginTailscaleSuccess": "已成功使用 Tailscale 进行身份验证。",
"loginTailscaleFail": "使用 Tailscale 进行身份验证失败。请重试或使用其他登录方式。",
"logoutTailscaleSubtitle": "您当前已通过 Tailscale 在名为<code>{{deviceName}}</code>的设备上登录。点击下方按钮注销。",
"quickActionsLanguage": "语言",
"quickActionsTheme": "主题",
"quickActionsThemeLight": "亮色主题",
"quickActionsThemeDark": "暗色主题",
"quickActionsThemeSystem": "跟随系统",
"quickActionsLogout": "登出",
"quickActionsTitle": "快速操作",
"quickActionsProviderLocal": "本地",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+24 -2
View File
@@ -71,7 +71,7 @@
"authorizeSuccessTitle": "已授權", "authorizeSuccessTitle": "已授權",
"authorizeSuccessSubtitle": "幾秒鐘內您將會被重新導向至應用程式。", "authorizeSuccessSubtitle": "幾秒鐘內您將會被重新導向至應用程式。",
"authorizeErrorClientInfo": "載入用戶端資訊時發生錯誤。請稍後再試。", "authorizeErrorClientInfo": "載入用戶端資訊時發生錯誤。請稍後再試。",
"authorizeErrorMissingParams": "下列參數遺失:{{missingParams}}", "authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
"openidScopeName": "OpenID 連接", "openidScopeName": "OpenID 連接",
"openidScopeDescription": "允許該應用程式存取您的 OpenID Connect 資訊。", "openidScopeDescription": "允許該應用程式存取您的 OpenID Connect 資訊。",
"emailScopeName": "電子郵件", "emailScopeName": "電子郵件",
@@ -79,5 +79,27 @@
"profileScopeName": "個人檔案", "profileScopeName": "個人檔案",
"profileScopeDescription": "允許該應用程式存取您的個人資料。", "profileScopeDescription": "允許該應用程式存取您的個人資料。",
"groupsScopeName": "群組", "groupsScopeName": "群組",
"groupsScopeDescription": "允許該應用程式存取您的群組資訊。" "groupsScopeDescription": "允許該應用程式存取您的群組資訊。",
"backToLoginButton": "Back to login",
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
"quickActionsLanguage": "Language",
"quickActionsTheme": "Theme",
"quickActionsThemeLight": "Light",
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+1 -1
View File
@@ -259,7 +259,7 @@ export const LoginPage = () => {
onClick={() => tailscaleMutate()} onClick={() => tailscaleMutate()}
loading={tailscaleIsPending} loading={tailscaleIsPending}
> >
{t("loginTailscaleSubmit")} {t("loginTailscaleTitle")}
</Button> </Button>
<Button <Button
className="w-full" className="w-full"
+4 -1
View File
@@ -46,11 +46,14 @@ func generateExampleEnv() {
func buildEnvEntry(child reflect.StructField, childValue reflect.Value, parentPath string, entries *[]EnvEntry) { func buildEnvEntry(child reflect.StructField, childValue reflect.Value, parentPath string, entries *[]EnvEntry) {
desc := child.Tag.Get("description") desc := child.Tag.Get("description")
tag := child.Tag.Get("yaml") tag := child.Tag.Get("yaml")
gen := child.Tag.Get("gen")
if tag == "-" { if tag == "-" && gen != "include" {
return return
} }
tag = strings.TrimSuffix(tag, ",omitempty")
value := childValue.Interface() value := childValue.Interface()
entry := EnvEntry{ entry := EnvEntry{
+61 -17
View File
@@ -1,7 +1,6 @@
package main package main
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
@@ -13,9 +12,13 @@ import (
"github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/model"
) )
type MarkdownEntry struct { type ConfigOptions struct {
Env string Env string
Flag string Flag string
YAML string
}
type MarkdownEntry struct {
Options ConfigOptions
Description string Description string
Default any Default any
} }
@@ -47,16 +50,27 @@ func generateMarkdown() {
func buildMdEntry(child reflect.StructField, childValue reflect.Value, parentPath string, entries *[]MarkdownEntry) { func buildMdEntry(child reflect.StructField, childValue reflect.Value, parentPath string, entries *[]MarkdownEntry) {
desc := child.Tag.Get("description") desc := child.Tag.Get("description")
tag := child.Tag.Get("yaml") tag := child.Tag.Get("yaml")
gen := child.Tag.Get("gen")
if tag == "-" { if tag == "-" && gen != "include" {
return return
} }
tag = strings.TrimSuffix(tag, ",omitempty")
value := childValue.Interface() value := childValue.Interface()
entry := MarkdownEntry{ configOptions := ConfigOptions{
Env: strings.ToUpper(strings.ReplaceAll(parentPath, ".", "_")) + strings.ToUpper(child.Name), Env: strings.ToUpper(strings.ReplaceAll(parentPath, ".", "_")) + strings.ToUpper(child.Name),
Flag: fmt.Sprintf("--%s%s", strings.TrimPrefix(parentPath, "tinyauth."), strings.ToLower(child.Name)), Flag: fmt.Sprintf("--%s%s", strings.TrimPrefix(parentPath, "tinyauth."), strings.ToLower(child.Name)),
}
if tag != "-" && tag != "" {
configOptions.YAML = strings.TrimPrefix(parentPath, "tinyauth.") + tag
}
entry := MarkdownEntry{
Options: configOptions,
Description: desc, Description: desc,
} }
@@ -83,11 +97,14 @@ func buildMdMapEntry(child reflect.StructField, parentPath string, entries *[]Ma
} }
tag := child.Tag.Get("yaml") tag := child.Tag.Get("yaml")
gen := child.Tag.Get("gen")
if tag == "-" { if tag == "-" && gen != "include" {
return return
} }
tag = strings.TrimSuffix(tag, ",omitempty")
mapPath := parentPath + tag + ".[name]." mapPath := parentPath + tag + ".[name]."
valueType := fieldType.Elem() valueType := fieldType.Elem()
@@ -102,27 +119,54 @@ func buildMdChildPath(parent string, child string) string {
} }
func compileMd(entries []MarkdownEntry) []byte { func compileMd(entries []MarkdownEntry) []byte {
buffer := bytes.Buffer{} buffer := strings.Builder{}
buffer.WriteString("<!--- This file is automatically generated by gen/gen_md.go. Do not edit manually. --->\n\n") buffer.WriteString("{/* This part is automatically generated by gen/gen_md.go in the main repository. DO NOT EDIT. */}\n\n")
buffer.WriteString("# Tinyauth configuration reference\n\n") buffer.WriteString("import { Tabs, TabItem } from '@astrojs/starlight/components';\n\n")
buffer.WriteString("| Environment | Flag | Description | Default |\n") buffer.WriteString("<Tabs>\n")
buffer.WriteString("| - | - | - | - |\n")
renderTabItem(&buffer, entries, "Environment")
renderTabItem(&buffer, entries, "Flags")
renderTabItem(&buffer, entries, "YAML")
buffer.WriteString("</Tabs>\n")
return []byte(buffer.String())
}
func renderTabItem(buffer *strings.Builder, entries []MarkdownEntry, section string) {
buffer.WriteString(fmt.Sprintf(" <TabItem label=\"%s\">\n", section))
buffer.WriteString(" ### main\n\n")
buffer.WriteString(" | Option | Description | Default |\n")
buffer.WriteString(" | - | - | - |\n")
configType := strings.ToLower(section)
previousSection := "" previousSection := ""
for _, entry := range entries { for _, entry := range entries {
if strings.Count(entry.Env, "_") > 1 { section := strings.Split(entry.Options.YAML, ".")[0]
section := strings.Split(strings.TrimPrefix(entry.Env, "TINYAUTH_"), "_")[0] var option string
switch configType {
case "yaml":
option = entry.Options.YAML
case "flags":
option = entry.Options.Flag
case "environment":
option = entry.Options.Env
}
if option == "" {
continue
}
if strings.Count(entry.Options.YAML, ".") >= 1 {
if section != previousSection { if section != previousSection {
buffer.WriteString("\n## " + strings.ToLower(section) + "\n\n") buffer.WriteString("\n ### " + strings.ToLower(section) + "\n\n")
buffer.WriteString("| Environment | Flag | Description | Default |\n") buffer.WriteString(" | Option | Description | Default |\n")
buffer.WriteString("| - | - | - | - |\n") buffer.WriteString(" | - | - | - |\n")
previousSection = section previousSection = section
} }
} }
fmt.Fprintf(&buffer, "| `%s` | `%s` | %s | %s |\n", entry.Env, entry.Flag, entry.Description, entry.Default) fmt.Fprintf(buffer, " | `%s` | %s | %s |\n", option, entry.Description, entry.Default)
} }
return buffer.Bytes() buffer.WriteString(" </TabItem>\n")
} }
+8 -8
View File
@@ -23,9 +23,9 @@ require (
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298 github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298
github.com/weppos/publicsuffix-go v0.50.3 github.com/weppos/publicsuffix-go v0.50.3
go.uber.org/dig v1.19.0 go.uber.org/dig v1.19.0
golang.org/x/crypto v0.53.0 golang.org/x/crypto v0.54.0
golang.org/x/oauth2 v0.36.0 golang.org/x/oauth2 v0.36.0
golang.org/x/tools v0.47.0 golang.org/x/tools v0.48.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
k8s.io/apimachinery v0.36.2 k8s.io/apimachinery v0.36.2
k8s.io/client-go v0.36.2 k8s.io/client-go v0.36.2
@@ -134,12 +134,12 @@ require (
go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/arch v0.22.0 // indirect golang.org/x/arch v0.22.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/mod v0.37.0 // indirect golang.org/x/mod v0.38.0 // indirect
golang.org/x/net v0.56.0 // indirect golang.org/x/net v0.57.0 // indirect
golang.org/x/sync v0.21.0 // indirect golang.org/x/sync v0.22.0 // indirect
golang.org/x/sys v0.46.0 // indirect golang.org/x/sys v0.47.0 // indirect
golang.org/x/term v0.44.0 // indirect golang.org/x/term v0.45.0 // indirect
golang.org/x/text v0.38.0 // indirect golang.org/x/text v0.40.0 // indirect
golang.org/x/time v0.14.0 // indirect golang.org/x/time v0.14.0 // indirect
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
+16 -16
View File
@@ -336,29 +336,29 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
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.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
+10
View File
@@ -30,6 +30,16 @@ func (app *BootstrapApp) setupRouter() error {
if err != nil { if err != nil {
return fmt.Errorf("failed to set trusted proxies: %w", err) return fmt.Errorf("failed to set trusted proxies: %w", err)
} }
app.runtime.TrustedProxiesConfigured = true
} else {
err := engine.SetTrustedProxies(nil)
if err != nil {
return fmt.Errorf("failed to set trusted proxies: %w", err)
}
app.log.App.Warn().Msg("Trusted proxies are not configured, IP access controls will NOT work")
} }
middlewareProvideFor := []any{ middlewareProvideFor := []any{
+29 -36
View File
@@ -1,9 +1,9 @@
package controller package controller
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"strings" "strings"
"time" "time"
@@ -12,6 +12,7 @@ import (
"github.com/tinyauthapp/tinyauth/internal/service" "github.com/tinyauthapp/tinyauth/internal/service"
"github.com/tinyauthapp/tinyauth/internal/utils" "github.com/tinyauthapp/tinyauth/internal/utils"
"github.com/tinyauthapp/tinyauth/internal/utils/logger" "github.com/tinyauthapp/tinyauth/internal/utils/logger"
"github.com/tinyauthapp/tinyauth/pkg/validators"
"go.uber.org/dig" "go.uber.org/dig"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -311,54 +312,46 @@ func (controller *OAuthController) getCookieDomain() string {
} }
func (controller *OAuthController) isRedirectSafe(redirectURI string) bool { func (controller *OAuthController) isRedirectSafe(redirectURI string) bool {
u, err := url.Parse(redirectURI) v := validators.NewDomainValidator(validators.DomainValidatorOptions{
WithScheme: true,
WithPort: true,
})
_, err := v.SafeHostname(controller.runtime.AppURL)
if err != nil { if err != nil {
controller.log.App.Error().Err(err).Msg("Failed to parse redirect URI") controller.log.App.Error().Err(err).Msg("App URL is invalid, cannot validate redirect URI")
return false return false
} }
if u.Scheme == "" || u.Host == "" { err = v.Validate(redirectURI, controller.runtime.AppURL)
controller.log.App.Warn().Msg("Redirect URI has invalid scheme or host")
return false
}
au, err := url.Parse(controller.runtime.AppURL) if err == nil {
if err != nil {
controller.log.App.Error().Err(err).Msg("Failed to parse app URL")
return false
}
if u.Scheme != au.Scheme {
controller.log.App.Warn().Msg("Redirect URI scheme does not match app URL scheme")
return false
}
getEffectivePort := func(u *url.URL) string {
if u.Port() != "" {
return u.Port()
}
if u.Scheme == "https" {
return "443"
}
return "80"
}
if getEffectivePort(u) != getEffectivePort(au) {
controller.log.App.Warn().Msg("Redirect URI port does not match app URL port")
return false
}
if strings.EqualFold(u.Hostname(), au.Hostname()) {
return true return true
} }
controller.log.App.Debug().Err(err).Msg("Failed to validate redirect URI")
if errors.Is(err, validators.ErrInvalidURL) ||
errors.Is(err, validators.ErrSchemeMismatch) ||
errors.Is(err, validators.ErrPortMismatch) {
return false
}
if !controller.config.Auth.SubdomainsEnabled { if !controller.config.Auth.SubdomainsEnabled {
return false return false
} }
if strings.HasSuffix(strings.ToLower(u.Hostname()), "."+strings.ToLower(controller.runtime.CookieDomain)) { v = validators.NewDomainValidator(validators.DomainValidatorOptions{})
hostname, err := v.SafeHostname(redirectURI)
if err != nil {
controller.log.App.Error().Err(err).Msg("Failed to get safe hostname from redirect URI")
return false
}
if strings.HasSuffix(hostname, "."+strings.ToLower(controller.runtime.CookieDomain)) {
return true return true
} }
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"github.com/tinyauthapp/tinyauth/internal/utils/logger" "github.com/tinyauthapp/tinyauth/internal/utils/logger"
) )
func TestOAuthControllerIsRedirectSafe(t *testing.T) { func TestOAuthController_isRedirectSafe(t *testing.T) {
log := logger.NewLogger().WithTestConfig() log := logger.NewLogger().WithTestConfig()
log.Init() log.Init()
+32 -27
View File
@@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
@@ -343,27 +344,31 @@ func (controller *OIDCController) authorizeComplete(c *gin.Context) {
// Create the authorization code // Create the authorization code
code := controller.oidc.CreateCode(*authorizeReq, *userContext) code := controller.oidc.CreateCode(*authorizeReq, *userContext)
queries, err := query.Values(AuthorizeCallback{ cu, err := url.Parse(authorizeReq.RedirectURI)
Code: code,
State: authorizeReq.State,
})
if err != nil { if err != nil {
controller.authorizeError(c, authorizeErrorParams{ controller.authorizeError(c, authorizeErrorParams{
err: err, err: err,
reason: "Failed to build query", reason: "Failed to parse redirect URI",
reasonPublic: "Failed to build query", reasonPublic: "Failed to parse redirect URI",
callback: authorizeReq.RedirectURI,
callbackError: "server_error",
state: authorizeReq.State,
json: true, json: true,
}) })
return return
} }
q := cu.Query()
q.Set("code", code)
if authorizeReq.State != "" {
q.Set("state", authorizeReq.State)
}
cu.RawQuery = q.Encode()
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"status": 200, "status": 200,
"redirect_uri": fmt.Sprintf("%s?%s", authorizeReq.RedirectURI, queries.Encode()), "redirect_uri": cu.String(),
}) })
} }
@@ -639,37 +644,37 @@ func (controller *OIDCController) authorizeError(c *gin.Context, params authoriz
controller.log.App.Error().Err(params.err).Str("reason", params.reason).Msg("Authorization error") controller.log.App.Error().Err(params.err).Str("reason", params.reason).Msg("Authorization error")
if params.callback != "" { if params.callback != "" {
errorQueries := CallbackError{ cu, err := url.Parse(params.callback)
Error: params.callbackError,
}
if params.reasonPublic != "" {
errorQueries.ErrorDescription = params.reasonPublic
}
if params.state != "" {
errorQueries.State = params.state
}
queries, err := query.Values(errorQueries)
if err != nil { if err != nil {
controller.log.App.Error().Err(err).Msg("Failed to build callback error query") controller.log.App.Error().Err(err).Msg("Failed to parse callback URL")
c.AbortWithStatus(http.StatusInternalServerError) c.AbortWithStatus(http.StatusInternalServerError)
return return
} }
redirectUrl := fmt.Sprintf("%s?%s", params.callback, queries.Encode()) q := cu.Query()
q.Set("error", params.callbackError)
if params.reasonPublic != "" {
q.Set("error_description", params.reasonPublic)
}
if params.state != "" {
q.Set("state", params.state)
}
cu.RawQuery = q.Encode()
if params.json { if params.json {
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"status": 200, "status": 200,
"redirect_uri": redirectUrl, "redirect_uri": cu.String(),
}) })
return return
} }
c.Redirect(http.StatusFound, redirectUrl) c.Redirect(http.StatusFound, cu.String())
return return
} }
+1
View File
@@ -114,6 +114,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
ACLs: acls, ACLs: acls,
IP: net.ParseIP(clientIP), IP: net.ParseIP(clientIP),
Path: proxyCtx.Path, Path: proxyCtx.Path,
TrustedProxiesConfigured: controller.runtime.TrustedProxiesConfigured,
} }
if controller.policyEngine.Evaluate(service.RuleIPBypassed, aclsCtx) { if controller.policyEngine.Evaluate(service.RuleIPBypassed, aclsCtx) {
+3 -1
View File
@@ -708,7 +708,7 @@ func TestProxyController(t *testing.T) {
Log: log, Log: log,
}) })
authService := service.NewAuthService(service.AuthServiceInput{ authService, err := service.NewAuthService(service.AuthServiceInput{
Log: log, Log: log,
Config: &cfg, Config: &cfg,
Runtime: &runtime, Runtime: &runtime,
@@ -721,6 +721,8 @@ func TestProxyController(t *testing.T) {
PolicyEngine: policyEngine, PolicyEngine: policyEngine,
}) })
require.NoError(t, err)
for _, test := range tests { for _, test := range tests {
t.Run(test.description, func(t *testing.T) { t.Run(test.description, func(t *testing.T) {
router := gin.Default() router := gin.Default()
+17 -25
View File
@@ -72,26 +72,12 @@ func (controller *UserController) loginHandler(c *gin.Context) {
controller.log.App.Debug().Str("username", req.Username).Msg("Login attempt") controller.log.App.Debug().Str("username", req.Username).Msg("Login attempt")
isLocked, remaining := controller.auth.IsAccountLocked(req.Username)
if isLocked {
controller.log.App.Warn().Str("username", req.Username).Msg("Account is locked due to too many failed login attempts")
controller.log.AuditLoginFailure(req.Username, "local", c.ClientIP(), "account locked")
c.Writer.Header().Add("x-tinyauth-lock-locked", "true")
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
c.JSON(429, gin.H{
"status": 429,
"message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remaining),
})
return
}
search, err := controller.auth.SearchUser(req.Username) search, err := controller.auth.SearchUser(req.Username)
if err != nil { if err != nil {
if errors.Is(err, service.ErrUserNotFound) { if errors.Is(err, service.ErrUserNotFound) {
controller.auth.DummyPasswordCheck(req.Password)
controller.log.App.Warn().Str("username", req.Username).Msg("User not found during login attempt") controller.log.App.Warn().Str("username", req.Username).Msg("User not found during login attempt")
controller.auth.RecordLoginAttempt(req.Username, false)
controller.log.AuditLoginFailure(req.Username, "unknown", c.ClientIP(), "user not found") controller.log.AuditLoginFailure(req.Username, "unknown", c.ClientIP(), "user not found")
c.JSON(401, gin.H{ c.JSON(401, gin.H{
"status": 401, "status": 401,
@@ -107,14 +93,24 @@ func (controller *UserController) loginHandler(c *gin.Context) {
return return
} }
isLocked, remaining := controller.auth.IsAccountLocked(req.Username)
if isLocked {
controller.log.App.Warn().Str("username", req.Username).Msg("Account is locked due to too many failed login attempts")
controller.log.AuditLoginFailure(req.Username, search.Type.String(), c.ClientIP(), "account locked")
c.Writer.Header().Add("x-tinyauth-lock-locked", "true")
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
c.JSON(429, gin.H{
"status": 429,
"message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remaining),
})
return
}
if err := controller.auth.CheckUserPassword(*search, req.Password); err != nil { if err := controller.auth.CheckUserPassword(*search, req.Password); err != nil {
controller.log.App.Warn().Str("username", req.Username).Msg("Invalid password during login attempt") controller.log.App.Warn().Str("username", req.Username).Msg("Invalid password during login attempt")
controller.auth.RecordLoginAttempt(req.Username, false) controller.auth.RecordLoginAttempt(req.Username, false)
if search.Type == model.UserLocal { controller.log.AuditLoginFailure(req.Username, search.Type.String(), c.ClientIP(), "invalid password")
controller.log.AuditLoginFailure(req.Username, "local", c.ClientIP(), "invalid password")
} else {
controller.log.AuditLoginFailure(req.Username, "ldap", c.ClientIP(), "invalid password")
}
c.JSON(401, gin.H{ c.JSON(401, gin.H{
"status": 401, "status": 401,
"message": "Unauthorized", "message": "Unauthorized",
@@ -215,11 +211,7 @@ func (controller *UserController) loginHandler(c *gin.Context) {
controller.log.App.Info().Str("username", req.Username).Msg("Login successful") controller.log.App.Info().Str("username", req.Username).Msg("Login successful")
if search.Type == model.UserLocal { controller.log.AuditLoginSuccess(req.Username, search.Type.String(), c.ClientIP())
controller.log.AuditLoginSuccess(req.Username, "local", c.ClientIP())
} else {
controller.log.AuditLoginSuccess(req.Username, "ldap", c.ClientIP())
}
controller.auth.RecordLoginAttempt(req.Username, true) controller.auth.RecordLoginAttempt(req.Username, true)
+4 -1
View File
@@ -542,7 +542,8 @@ func TestUserController(t *testing.T) {
Runtime: &runtime, Runtime: &runtime,
Ctx: ctx, Ctx: ctx,
}) })
authService := service.NewAuthService(service.AuthServiceInput{
authService, err := service.NewAuthService(service.AuthServiceInput{
Log: log, Log: log,
Config: &cfg, Config: &cfg,
Runtime: &runtime, Runtime: &runtime,
@@ -555,6 +556,8 @@ func TestUserController(t *testing.T) {
PolicyEngine: policyEngine, PolicyEngine: policyEngine,
}) })
require.NoError(t, err)
beforeEach := func() { beforeEach := func() {
// Clear failed login attempts before each test // Clear failed login attempts before each test
authService.ClearLoginAttempts() authService.ClearLoginAttempts()
@@ -2,6 +2,7 @@ package middleware
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
@@ -244,6 +245,9 @@ func (m *ContextMiddleware) basicAuth(username string, password string) (*model.
search, err := m.auth.SearchUser(username) search, err := m.auth.SearchUser(username)
if err != nil { if err != nil {
if errors.Is(err, service.ErrUserNotFound) {
m.auth.DummyPasswordCheck(password)
}
return nil, nil, fmt.Errorf("error searching for user: %w", err) return nil, nil, fmt.Errorf("error searching for user: %w", err)
} }
@@ -264,7 +264,8 @@ func TestContextMiddleware(t *testing.T) {
Runtime: &runtime, Runtime: &runtime,
Ctx: ctx, Ctx: ctx,
}) })
authService := service.NewAuthService(service.AuthServiceInput{
authService, err := service.NewAuthService(service.AuthServiceInput{
Log: log, Log: log,
Config: &cfg, Config: &cfg,
Runtime: &runtime, Runtime: &runtime,
@@ -277,6 +278,8 @@ func TestContextMiddleware(t *testing.T) {
PolicyEngine: policyEngine, PolicyEngine: policyEngine,
}) })
require.NoError(t, err)
contextMiddleware := NewContextMiddleware(ContextMiddlewareInput{ contextMiddleware := NewContextMiddleware(ContextMiddlewareInput{
Log: log, Log: log,
RuntimeConfig: &runtime, RuntimeConfig: &runtime,
+2 -4
View File
@@ -49,7 +49,6 @@ func NewDefaultConfiguration(runtimeEnv RuntimeEnv) *Config {
ACLs: ACLsConfig{ ACLs: ACLsConfig{
Policy: "allow", Policy: "allow",
}, },
LockdownEnabled: true,
}, },
UI: UIConfig{ UI: UIConfig{
Title: "Tinyauth", Title: "Tinyauth",
@@ -103,6 +102,8 @@ func NewDefaultConfiguration(runtimeEnv RuntimeEnv) *Config {
type Config struct { type Config struct {
AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl,omitempty"` AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl,omitempty"`
ConfigFile string `description:"Path to config file." yaml:"-" gen:"include"`
LabelProvider string `description:"Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment." yaml:"labelProvider,omitempty"`
Database DatabaseConfig `description:"Database configuration." yaml:"database,omitempty"` Database DatabaseConfig `description:"Database configuration." yaml:"database,omitempty"`
Analytics AnalyticsConfig `description:"Analytics configuration." yaml:"analytics,omitempty"` Analytics AnalyticsConfig `description:"Analytics configuration." yaml:"analytics,omitempty"`
Resources ResourcesConfig `description:"Resources configuration." yaml:"resources,omitempty"` Resources ResourcesConfig `description:"Resources configuration." yaml:"resources,omitempty"`
@@ -115,9 +116,7 @@ type Config struct {
LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap,omitempty"` LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap,omitempty"`
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental,omitempty"` Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental,omitempty"`
Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale,omitempty"` Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale,omitempty"`
LabelProvider string `description:"Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment." yaml:"labelProvider,omitempty"`
Log LogConfig `description:"Logging configuration." yaml:"log,omitempty"` Log LogConfig `description:"Logging configuration." yaml:"log,omitempty"`
ConfigFile string `description:"Path to config file." yaml:"-"`
} }
type DatabaseConfig struct { type DatabaseConfig struct {
@@ -151,7 +150,6 @@ type AuthConfig struct {
SessionMaxLifetime int `description:"Maximum session lifetime in seconds." yaml:"sessionMaxLifetime,omitempty"` SessionMaxLifetime int `description:"Maximum session lifetime in seconds." yaml:"sessionMaxLifetime,omitempty"`
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout,omitempty"` LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout,omitempty"`
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries,omitempty"` LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries,omitempty"`
LockdownEnabled bool `description:"Enable lockdown mode after maximum login retries. Lockdown mode limit is calculated automatically." yaml:"lockdownEnabled,omitempty"`
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies,omitempty"` TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies,omitempty"`
ACLs ACLsConfig `description:"ACLs configuration." yaml:"acls,omitempty"` ACLs ACLsConfig `description:"ACLs configuration." yaml:"acls,omitempty"`
} }
+1
View File
@@ -12,6 +12,7 @@ type RuntimeConfig struct {
OAuthProviders map[string]OAuthServiceConfig OAuthProviders map[string]OAuthServiceConfig
OAuthWhitelist []string OAuthWhitelist []string
ConfiguredProviders []Provider ConfiguredProviders []Provider
TrustedProxiesConfigured bool
} }
type Provider struct { type Provider struct {
+10
View File
@@ -7,6 +7,16 @@ const (
UserLDAP UserLDAP
) )
func (t UserSearchType) String() string {
switch t {
case UserLocal:
return "local"
case UserLDAP:
return "ldap"
}
return "unknown"
}
type LDAPUser struct { type LDAPUser struct {
DN string DN string
Groups []string Groups []string
@@ -215,6 +215,10 @@ type IPAllowedRule struct {
} }
func (rule *IPAllowedRule) Evaluate(ctx *ACLContext) Effect { func (rule *IPAllowedRule) Evaluate(ctx *ACLContext) Effect {
if !ctx.TrustedProxiesConfigured {
return EffectAllow // We can't block the proxy
}
// merge global and per-app block/allow lists // merge global and per-app block/allow lists
blockedIps := append([]string{}, rule.Config.Auth.IP.Block...) blockedIps := append([]string{}, rule.Config.Auth.IP.Block...)
allowedIPs := append([]string{}, rule.Config.Auth.IP.Allow...) allowedIPs := append([]string{}, rule.Config.Auth.IP.Allow...)
@@ -263,6 +267,10 @@ type IPBypassedRule struct {
} }
func (rule *IPBypassedRule) Evaluate(ctx *ACLContext) Effect { func (rule *IPBypassedRule) Evaluate(ctx *ACLContext) Effect {
if !ctx.TrustedProxiesConfigured {
return EffectDeny
}
// merge global and per-app bypass lists // merge global and per-app bypass lists
bypassList := append([]string{}, rule.Config.Auth.IP.Bypass...) bypassList := append([]string{}, rule.Config.Auth.IP.Bypass...)
if ctx.ACLs != nil { if ctx.ACLs != nil {
@@ -611,11 +611,20 @@ func TestIPAllowedRule(t *testing.T) {
ctx *ACLContext ctx *ACLContext
expected Effect expected Effect
}{ }{
{
name: "when trusted proxies are not configured, IP is allowed",
ctx: &ACLContext{
ACLs: &model.App{},
IP: net.ParseIP("10.0.0.1"),
},
expected: EffectAllow,
},
{ {
name: "allows when ACLs are nil and no global lists configured", name: "allows when ACLs are nil and no global lists configured",
ctx: &ACLContext{ ctx: &ACLContext{
ACLs: nil, ACLs: nil,
IP: net.ParseIP("10.0.0.1"), IP: net.ParseIP("10.0.0.1"),
TrustedProxiesConfigured: true,
}, },
expected: EffectAllow, expected: EffectAllow,
}, },
@@ -626,6 +635,7 @@ func TestIPAllowedRule(t *testing.T) {
IP: model.AppIP{Block: []string{"10.0.0.1"}}, IP: model.AppIP{Block: []string{"10.0.0.1"}},
}, },
IP: net.ParseIP("10.0.0.1"), IP: net.ParseIP("10.0.0.1"),
TrustedProxiesConfigured: true,
}, },
expected: EffectDeny, expected: EffectDeny,
}, },
@@ -639,6 +649,7 @@ func TestIPAllowedRule(t *testing.T) {
ctx: &ACLContext{ ctx: &ACLContext{
ACLs: &model.App{}, ACLs: &model.App{},
IP: net.ParseIP("10.0.0.5"), IP: net.ParseIP("10.0.0.5"),
TrustedProxiesConfigured: true,
}, },
expected: EffectDeny, expected: EffectDeny,
}, },
@@ -649,6 +660,7 @@ func TestIPAllowedRule(t *testing.T) {
IP: model.AppIP{Allow: []string{"192.168.1.0/24"}}, IP: model.AppIP{Allow: []string{"192.168.1.0/24"}},
}, },
IP: net.ParseIP("192.168.1.10"), IP: net.ParseIP("192.168.1.10"),
TrustedProxiesConfigured: true,
}, },
expected: EffectAllow, expected: EffectAllow,
}, },
@@ -662,6 +674,7 @@ func TestIPAllowedRule(t *testing.T) {
ctx: &ACLContext{ ctx: &ACLContext{
ACLs: &model.App{}, ACLs: &model.App{},
IP: net.ParseIP("192.168.1.10"), IP: net.ParseIP("192.168.1.10"),
TrustedProxiesConfigured: true,
}, },
expected: EffectAllow, expected: EffectAllow,
}, },
@@ -672,6 +685,7 @@ func TestIPAllowedRule(t *testing.T) {
IP: model.AppIP{Allow: []string{"192.168.1.0/24"}}, IP: model.AppIP{Allow: []string{"192.168.1.0/24"}},
}, },
IP: net.ParseIP("10.0.0.1"), IP: net.ParseIP("10.0.0.1"),
TrustedProxiesConfigured: true,
}, },
expected: EffectDeny, expected: EffectDeny,
}, },
@@ -680,6 +694,7 @@ func TestIPAllowedRule(t *testing.T) {
ctx: &ACLContext{ ctx: &ACLContext{
ACLs: &model.App{}, ACLs: &model.App{},
IP: net.ParseIP("10.0.0.1"), IP: net.ParseIP("10.0.0.1"),
TrustedProxiesConfigured: true,
}, },
expected: EffectAllow, expected: EffectAllow,
}, },
@@ -693,6 +708,7 @@ func TestIPAllowedRule(t *testing.T) {
}, },
}, },
IP: net.ParseIP("10.0.0.1"), IP: net.ParseIP("10.0.0.1"),
TrustedProxiesConfigured: true,
}, },
expected: EffectDeny, expected: EffectDeny,
}, },
@@ -706,6 +722,7 @@ func TestIPAllowedRule(t *testing.T) {
}, },
}, },
IP: net.ParseIP("10.0.0.1"), IP: net.ParseIP("10.0.0.1"),
TrustedProxiesConfigured: true,
}, },
expected: EffectAllow, expected: EffectAllow,
}, },
@@ -735,12 +752,23 @@ func TestIPBypassedRule(t *testing.T) {
ctx *ACLContext ctx *ACLContext
expected Effect expected Effect
}{ }{
{
name: "when trusted proxies are not configured, IP is not bypassed",
rule: defaultIPBR,
ctx: &ACLContext{
ACLs: &model.App{},
IP: net.ParseIP("10.0.0.1"),
TrustedProxiesConfigured: false,
},
expected: EffectDeny,
},
{ {
name: "deny when ACLs are nil and no global bypass", name: "deny when ACLs are nil and no global bypass",
rule: defaultIPBR, rule: defaultIPBR,
ctx: &ACLContext{ ctx: &ACLContext{
ACLs: nil, ACLs: nil,
IP: net.ParseIP("10.0.0.1"), IP: net.ParseIP("10.0.0.1"),
TrustedProxiesConfigured: true,
}, },
expected: EffectDeny, expected: EffectDeny,
}, },
@@ -750,6 +778,7 @@ func TestIPBypassedRule(t *testing.T) {
ctx: &ACLContext{ ctx: &ACLContext{
ACLs: nil, ACLs: nil,
IP: net.ParseIP("10.0.0.5"), IP: net.ParseIP("10.0.0.5"),
TrustedProxiesConfigured: true,
}, },
expected: EffectAllow, expected: EffectAllow,
}, },
@@ -759,6 +788,7 @@ func TestIPBypassedRule(t *testing.T) {
ctx: &ACLContext{ ctx: &ACLContext{
ACLs: nil, ACLs: nil,
IP: net.ParseIP("192.168.1.1"), IP: net.ParseIP("192.168.1.1"),
TrustedProxiesConfigured: true,
}, },
expected: EffectDeny, expected: EffectDeny,
}, },
@@ -770,6 +800,7 @@ func TestIPBypassedRule(t *testing.T) {
IP: model.AppIP{Bypass: []string{"10.0.0.0/24"}}, IP: model.AppIP{Bypass: []string{"10.0.0.0/24"}},
}, },
IP: net.ParseIP("10.0.0.5"), IP: net.ParseIP("10.0.0.5"),
TrustedProxiesConfigured: true,
}, },
expected: EffectAllow, expected: EffectAllow,
}, },
@@ -781,6 +812,7 @@ func TestIPBypassedRule(t *testing.T) {
IP: model.AppIP{Bypass: []string{"172.16.0.0/24"}}, IP: model.AppIP{Bypass: []string{"172.16.0.0/24"}},
}, },
IP: net.ParseIP("10.0.0.5"), IP: net.ParseIP("10.0.0.5"),
TrustedProxiesConfigured: true,
}, },
expected: EffectAllow, expected: EffectAllow,
}, },
@@ -792,6 +824,7 @@ func TestIPBypassedRule(t *testing.T) {
IP: model.AppIP{Bypass: []string{"10.0.0.0/24"}}, IP: model.AppIP{Bypass: []string{"10.0.0.0/24"}},
}, },
IP: net.ParseIP("10.0.0.5"), IP: net.ParseIP("10.0.0.5"),
TrustedProxiesConfigured: true,
}, },
expected: EffectAllow, expected: EffectAllow,
}, },
@@ -803,6 +836,7 @@ func TestIPBypassedRule(t *testing.T) {
IP: model.AppIP{Bypass: []string{"10.0.0.0/24"}}, IP: model.AppIP{Bypass: []string{"10.0.0.0/24"}},
}, },
IP: net.ParseIP("192.168.1.1"), IP: net.ParseIP("192.168.1.1"),
TrustedProxiesConfigured: true,
}, },
expected: EffectDeny, expected: EffectDeny,
}, },
@@ -812,6 +846,7 @@ func TestIPBypassedRule(t *testing.T) {
ctx: &ACLContext{ ctx: &ACLContext{
ACLs: &model.App{}, ACLs: &model.App{},
IP: net.ParseIP("10.0.0.1"), IP: net.ParseIP("10.0.0.1"),
TrustedProxiesConfigured: true,
}, },
expected: EffectDeny, expected: EffectDeny,
}, },
@@ -823,6 +858,7 @@ func TestIPBypassedRule(t *testing.T) {
IP: model.AppIP{Bypass: []string{"not-an-ip", "10.0.0.1"}}, IP: model.AppIP{Bypass: []string{"not-an-ip", "10.0.0.1"}},
}, },
IP: net.ParseIP("10.0.0.1"), IP: net.ParseIP("10.0.0.1"),
TrustedProxiesConfigured: true,
}, },
expected: EffectAllow, expected: EffectAllow,
}, },
+10 -2
View File
@@ -1,10 +1,12 @@
package service package service
import ( import (
"errors"
"strings" "strings"
"github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/logger" "github.com/tinyauthapp/tinyauth/internal/utils/logger"
"github.com/tinyauthapp/tinyauth/pkg/validators"
"go.uber.org/dig" "go.uber.org/dig"
) )
@@ -38,13 +40,19 @@ func NewAccessControlsService(i AccessControlServiceInput) *AccessControlsServic
func (service *AccessControlsService) lookupStaticACLs(domain string) *model.App { func (service *AccessControlsService) lookupStaticACLs(domain string) *model.App {
var nameMatch *model.App var nameMatch *model.App
v := validators.NewDomainValidator(validators.DomainValidatorOptions{})
// First try to find a matching app by domain, then fallback to matching by app name (subdomain) // First try to find a matching app by domain, then fallback to matching by app name (subdomain)
for app, config := range service.config.Apps { for app, config := range service.config.Apps {
if config.Config.Domain == domain { err := v.Validate(config.Config.Domain, domain)
if err == nil {
service.log.App.Debug().Str("name", app).Msg("Found matching container by domain") service.log.App.Debug().Str("name", app).Msg("Found matching container by domain")
return &config return &config
} }
if strings.SplitN(domain, ".", 2)[0] == app { if !errors.Is(err, validators.ErrHostnameMismatch) {
service.log.App.Debug().Str("name", app).Err(err).Msg("Domain validation failed")
}
if strings.HasPrefix(strings.ToLower(domain), strings.ToLower(app+".")) {
service.log.App.Debug().Str("name", app).Msg("Found matching container by app name") service.log.App.Debug().Str("name", app).Msg("Found matching container by app name")
nameMatch = &config nameMatch = &config
} }
+34 -89
View File
@@ -8,7 +8,6 @@ import (
"math/big" "math/big"
"net/http" "net/http"
"strings" "strings"
"sync"
"time" "time"
"github.com/steveiliop56/ding" "github.com/steveiliop56/ding"
@@ -69,21 +68,13 @@ type AuthService struct {
tailscale *TailscaleService tailscale *TailscaleService
policyEngine *PolicyEngine policyEngine *PolicyEngine
lockdown struct { dummyHash string
active bool
until time.Time
ctx context.Context
cancelFunc context.CancelFunc
mu sync.RWMutex
}
caches struct { caches struct {
login *CacheStore[LoginAttempt] login *CacheStore[LoginAttempt]
oauth *CacheStore[OAuthPendingSession] oauth *CacheStore[OAuthPendingSession]
ldap *CacheStore[[]string] ldap *CacheStore[[]string]
} }
maxLoginLimits int
} }
type AuthServiceInput struct { type AuthServiceInput struct {
@@ -101,7 +92,7 @@ type AuthServiceInput struct {
PolicyEngine *PolicyEngine PolicyEngine *PolicyEngine
} }
func NewAuthService(i AuthServiceInput) *AuthService { func NewAuthService(i AuthServiceInput) (*AuthService, error) {
service := &AuthService{ service := &AuthService{
log: i.Log, log: i.Log,
runtime: i.Runtime, runtime: i.Runtime,
@@ -114,18 +105,18 @@ func NewAuthService(i AuthServiceInput) *AuthService {
policyEngine: i.PolicyEngine, policyEngine: i.PolicyEngine,
} }
// get the max login limits based on the number of users and the configured max retries // dummy hash
service.maxLoginLimits = service.calculateLockdownLimit() dummyHash, err := bcrypt.GenerateFromPassword([]byte(utils.GenerateString(8)), bcrypt.DefaultCost)
loginCacheSize := 0 if err != nil {
return nil, fmt.Errorf("failed to generate dummy hash: %w", err)
if !service.config.Auth.LockdownEnabled {
loginCacheSize = service.maxLoginLimits
} }
service.dummyHash = string(dummyHash)
// caches setup // caches setup
oauthCache := NewCacheStore[OAuthPendingSession](256) oauthCache := NewCacheStore[OAuthPendingSession](256)
loginCache := NewCacheStore[LoginAttempt](loginCacheSize) loginCache := NewCacheStore[LoginAttempt](service.calculateLockdownLimit())
ldapCache := NewCacheStore[[]string](1024) ldapCache := NewCacheStore[[]string](1024)
service.caches.oauth = oauthCache service.caches.oauth = oauthCache
@@ -148,7 +139,28 @@ func NewAuthService(i AuthServiceInput) *AuthService {
} }
}, ding.RingMinor) }, ding.RingMinor)
return service i.Ding.Go(func(ctx context.Context) {
ticker := time.NewTicker(15 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ticker.C:
service.log.App.Debug().Msg("Updating login cache limits")
service.caches.login.SetMaxSize(service.calculateLockdownLimit())
service.log.App.Debug().Msg("Login cache limits updated")
case <-ctx.Done():
return
}
}
}, ding.RingMinor)
return service, nil
}
func (auth *AuthService) DummyPasswordCheck(password string) {
bcrypt.CompareHashAndPassword([]byte(auth.dummyHash), []byte(password))
} }
func (auth *AuthService) SearchUser(username string) (*model.UserSearch, error) { func (auth *AuthService) SearchUser(username string) (*model.UserSearch, error) {
@@ -245,10 +257,6 @@ func (auth *AuthService) GetLDAPUser(userDN string) (*model.LDAPUser, error) {
} }
func (auth *AuthService) IsAccountLocked(identifier string) (bool, int) { func (auth *AuthService) IsAccountLocked(identifier string) (bool, int) {
if locked, remaining := auth.IsInLockdown(); locked {
return true, remaining
}
if auth.config.Auth.LoginMaxRetries <= 0 || auth.config.Auth.LoginTimeout <= 0 { if auth.config.Auth.LoginMaxRetries <= 0 || auth.config.Auth.LoginTimeout <= 0 {
return false, 0 return false, 0
} }
@@ -271,14 +279,6 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) {
return return
} }
if !success && auth.config.Auth.LockdownEnabled && auth.caches.login.Size() >= auth.maxLoginLimits {
if locked, _ := auth.IsInLockdown(); locked {
return
}
go auth.lockdownMode()
return
}
auth.caches.login.WithLock(func(actions CacheStoreActions[LoginAttempt]) { auth.caches.login.WithLock(func(actions CacheStoreActions[LoginAttempt]) {
entry, ok := actions.Get(identifier) entry, ok := actions.Get(identifier)
@@ -344,7 +344,7 @@ func (auth *AuthService) CreateSession(ctx context.Context, data repository.Sess
return nil, fmt.Errorf("tailscale service not configured, cannot create session for tailscale user") return nil, fmt.Errorf("tailscale service not configured, cannot create session for tailscale user")
} }
uuid, err := uuid.NewRandom() u, err := uuid.NewRandom()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to generate session uuid: %w", err) return nil, fmt.Errorf("failed to generate session uuid: %w", err)
@@ -361,7 +361,7 @@ func (auth *AuthService) CreateSession(ctx context.Context, data repository.Sess
expiresAt := time.Now().Add(time.Duration(expiry) * time.Second) expiresAt := time.Now().Add(time.Duration(expiry) * time.Second)
session := repository.CreateSessionParams{ session := repository.CreateSessionParams{
UUID: uuid.String(), UUID: u.String(),
Username: data.Username, Username: data.Username,
Email: data.Email, Email: data.Email,
Name: data.Name, Name: data.Name,
@@ -616,62 +616,7 @@ func (auth *AuthService) GetOAuthPendingSession(sessionId string) (*OAuthPending
return &session, nil return &session, nil
} }
func (auth *AuthService) lockdownMode() { // ClearLoginAttempts is a testing function, not useful for anything else
auth.lockdown.mu.Lock()
if auth.lockdown.active {
auth.lockdown.mu.Unlock()
return
}
ctx, cancel := context.WithCancel(auth.ctx)
auth.log.App.Warn().Msg("Too many failed login attempts, entering lockdown mode")
auth.lockdown.active = true
auth.lockdown.ctx = ctx
auth.lockdown.cancelFunc = cancel
d := time.Duration(auth.config.Auth.LoginTimeout) * time.Second
auth.lockdown.until = time.Now().Add(d)
timer := time.NewTimer(d)
auth.lockdown.mu.Unlock()
defer cancel()
defer timer.Stop()
select {
case <-timer.C:
// Timer expired, end lockdown
case <-ctx.Done():
// Context cancelled, end lockdown
}
auth.lockdown.mu.Lock()
auth.log.App.Info().Msg("Exiting lockdown mode")
auth.caches.login.Clear()
auth.lockdown.active = false
auth.lockdown.until = time.Time{}
auth.lockdown.ctx = nil
auth.lockdown.cancelFunc = nil
auth.lockdown.mu.Unlock()
}
func (auth *AuthService) IsInLockdown() (bool, int) {
auth.lockdown.mu.RLock()
defer auth.lockdown.mu.RUnlock()
if auth.lockdown.active {
remaining := int(time.Until(auth.lockdown.until).Seconds())
return true, remaining
}
return false, 0
}
// mostly a testing function, not useful for anything else
func (auth *AuthService) ClearLoginAttempts() { func (auth *AuthService) ClearLoginAttempts() {
auth.caches.login.Clear() auth.caches.login.Clear()
} }
+16
View File
@@ -195,3 +195,19 @@ func (cs *CacheStore[T]) Clear() {
cs.cache = make(map[string]cacheEntry[T]) cs.cache = make(map[string]cacheEntry[T])
cs.order = make([]string, 0) cs.order = make([]string, 0)
} }
func (cs *CacheStore[T]) SetMaxSize(maxSize int) {
cs.mu.Lock()
defer cs.mu.Unlock()
cs.maxSize = maxSize
for len(cs.cache) > maxSize {
if !cs.evictOne() {
break
}
}
}
func (cs *CacheStore[T]) GetMaxSize() int {
cs.mu.Lock()
defer cs.mu.Unlock()
return cs.maxSize
}
+15
View File
@@ -316,6 +316,21 @@ func TestCacheStoreSizeAndClear(t *testing.T) {
assert.False(t, ok) assert.False(t, ok)
} }
func TestCacheStoreWithMaxSize(t *testing.T) {
cs := NewCacheStore[string](0)
assert.Equal(t, 0, cs.Size())
for i := 0; i < 100; i++ {
cs.Set(strconv.Itoa(i), strconv.Itoa(i), 0)
}
assert.Equal(t, 100, cs.Size())
cs.SetMaxSize(10)
assert.Equal(t, 10, cs.Size())
}
func TestCacheStoreWithLock(t *testing.T) { func TestCacheStoreWithLock(t *testing.T) {
cs := NewCacheStore[int](0) cs := NewCacheStore[int](0)
cs.Set("counter", 1, 0) cs.Set("counter", 1, 0)
+1
View File
@@ -24,6 +24,7 @@ func NewOAuthService(config model.OAuthServiceConfig, id string, ctx context.Con
httpClient := &http.Client{ httpClient := &http.Client{
Timeout: 30 * time.Second, Timeout: 30 * time.Second,
Transport: &http.Transport{ Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{ TLSClientConfig: &tls.Config{
InsecureSkipVerify: config.Insecure, InsecureSkipVerify: config.Insecure,
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
+1
View File
@@ -33,6 +33,7 @@ type ACLContext struct {
UserContext *model.UserContext UserContext *model.UserContext
IP net.IP IP net.IP
Path string Path string
TrustedProxiesConfigured bool
} }
type PolicyEngine struct { type PolicyEngine struct {
+1
View File
@@ -166,6 +166,7 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) {
CookieDomain: "example.com", CookieDomain: "example.com",
AppURL: "https://tinyauth.example.com", AppURL: "https://tinyauth.example.com",
SessionCookieName: "tinyauth-session", SessionCookieName: "tinyauth-session",
TrustedProxiesConfigured: true,
} }
return config, runtime return config, runtime
+179
View File
@@ -0,0 +1,179 @@
// Package validators provides validators for various types of data.
//
// Domain validator is a simple utility that ensures two domains are exact
// matches while ensuring that techniques used to bypass such checks do
// not impact the validation.
package validators
import (
"fmt"
"net"
"net/url"
"slices"
"strings"
"golang.org/x/net/idna"
)
var (
ErrInvalidURL = fmt.Errorf("invalid url")
ErrSchemeMismatch = fmt.Errorf("scheme mismatch")
ErrPortMismatch = fmt.Errorf("port mismatch")
ErrHostnameMismatch = fmt.Errorf("hostname mismatch")
)
// DomainValidatorOptions is a set of options for DomainValidator.
type DomainValidatorOptions struct {
// Ensure domains have the same scheme.
WithScheme bool
// Ensure domains have the same port.
WithPort bool
// Specify a list of allowed schemes IF WithScheme is set to true.
// Leave empty to allow any scheme.
AllowedSchemes []string
}
// DomainValidator is a simple utility that ensures two domains are exact
// matches while ensuring that techniques used to bypass such checks do
// not impact the validation.
type DomainValidator struct {
opts DomainValidatorOptions
}
// NewDomainValidator creates a new DomainValidator.
func NewDomainValidator(opts DomainValidatorOptions) *DomainValidator {
return &DomainValidator{
opts: opts,
}
}
func (v *DomainValidator) getURL(i string) (*url.URL, error) {
u, err := url.Parse(i)
if !v.opts.WithScheme && (err != nil || u.Host == "") {
u, err = url.Parse("tinyauth://" + i)
}
if err != nil {
return nil, fmt.Errorf("failed to parse input url: %w", err)
}
if u.Host == "" {
return nil, ErrInvalidURL
}
if v.opts.WithPort && !v.opts.WithScheme && u.Port() == "" {
return nil, fmt.Errorf("port validation is enabled but port is missing in input url and schemes are not enabled")
}
if v.opts.WithScheme {
// Empty scheme means that we parsed the url with the tinyauth:// placeholder
if u.Scheme == "tinyauth" {
return nil, fmt.Errorf("input url is missing scheme")
}
if len(v.opts.AllowedSchemes) > 0 && !slices.Contains(v.opts.AllowedSchemes, u.Scheme) {
return nil, fmt.Errorf("scheme %s not allowed", u.Scheme)
}
}
return u, nil
}
func (v *DomainValidator) getEffectivePort(u *url.URL) (string, bool) {
if u.Port() != "" {
return u.Port(), true
}
switch u.Scheme {
case "http":
return "80", true
case "https":
return "443", true
default:
return "", false
}
}
func (v *DomainValidator) formatHostname(hostname string) (string, error) {
hostname = strings.ToLower(hostname)
hostname = strings.TrimSuffix(hostname, ".")
if net.ParseIP(hostname) != nil {
return "", fmt.Errorf("ip addresses are not supported")
}
hostname, err := idna.Lookup.ToASCII(hostname)
if err != nil {
return "", fmt.Errorf("failed to convert hostname to ascii: %w", err)
}
return hostname, nil
}
// Validate ensures that two domains are exact matches with the
// options defined in the DomainValidatorOptions. It ensures that the
// inputs are proper URLs and contain a host. It lowercases the hostnames
// and removes the trailing dot. Finally, it checks that the hostnames are
// equal unless WithScheme or WithPort is set to true where it also
// validates the scheme and port respectively.
func (v *DomainValidator) Validate(expected, actual string) error {
eu, err := v.getURL(expected)
if err != nil {
return err
}
au, err := v.getURL(actual)
if err != nil {
return err
}
if v.opts.WithScheme {
if eu.Scheme != au.Scheme {
return ErrSchemeMismatch
}
}
if v.opts.WithPort {
eup, ok := v.getEffectivePort(eu)
if !ok {
return fmt.Errorf("failed to get effective port for url: %s", eu.String())
}
aup, ok := v.getEffectivePort(au)
if !ok {
return fmt.Errorf("failed to get effective port for url: %s", au.String())
}
if eup != aup {
return ErrPortMismatch
}
}
euf, err := v.formatHostname(eu.Hostname())
if err != nil {
return err
}
auf, err := v.formatHostname(au.Hostname())
if err != nil {
return err
}
if euf != auf {
return ErrHostnameMismatch
}
return nil
}
// SafeHostname uses the internal validation for domains that Validator uses
// to parse a hostname. It ensures the input URL is a valid URL, that a host
// is present and that the hostname is lowercased and without a trailing dot.
func (v *DomainValidator) SafeHostname(input string) (string, error) {
u, err := v.getURL(input)
if err != nil {
return "", err
}
return v.formatHostname(u.Hostname())
}
+288
View File
@@ -0,0 +1,288 @@
package validators
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDomainValidator_SafeHostname(t *testing.T) {
type testCase struct {
description string
options DomainValidatorOptions
input string
expected string
errorFunc func(t *testing.T, e error)
}
tests := []testCase{
{
description: "Empty url fails",
errorFunc: func(t *testing.T, e error) {
assert.ErrorIs(t, e, ErrInvalidURL)
},
},
{
description: "Invalid url fails",
input: "foo:foo",
errorFunc: func(t *testing.T, e error) {
assert.ErrorContains(t, e, "failed to parse input url")
},
},
{
description: "Domain without scheme should parse if scheme is disabled",
input: "example.com",
expected: "example.com",
},
{
description: "Domain without scheme should not parse if scheme is enabled",
options: DomainValidatorOptions{WithScheme: true},
input: "example.com",
errorFunc: func(t *testing.T, e error) {
assert.ErrorIs(t, e, ErrInvalidURL)
},
},
{
description: "Domain with scheme and disallowed scheme should fail",
options: DomainValidatorOptions{WithScheme: true, AllowedSchemes: []string{"https"}},
input: "foo://example.com",
errorFunc: func(t *testing.T, e error) {
assert.ErrorContains(t, e, "foo not allowed")
},
},
{
description: "Domain with scheme and allowed scheme should pass",
options: DomainValidatorOptions{WithScheme: true, AllowedSchemes: []string{"https"}},
input: "https://example.com",
expected: "example.com",
},
{
description: "Domain should get lowercased",
input: "EXAMPLE.COM",
expected: "example.com",
},
{
description: "DNS dot should be removed",
input: "example.com.",
expected: "example.com",
},
{
description: "IPv4 address should fail",
input: "127.0.0.1",
errorFunc: func(t *testing.T, e error) {
assert.ErrorContains(t, e, "ip addresses are not supported")
},
},
{
description: "IPv6 address should fail",
input: "[::1]",
errorFunc: func(t *testing.T, e error) {
assert.ErrorContains(t, e, "ip addresses are not supported")
},
},
{
description: "Domains with unicode characters should be allowed",
input: "bücher.example.com",
expected: "xn--bcher-kva.example.com",
},
{
description: "Invalid IDNA domain should fail",
input: "ab--cd.example.com",
errorFunc: func(t *testing.T, e error) {
assert.ErrorContains(t, e, "invalid label")
},
},
{
// Placeholder should not be used by users and is reserved for the validator.
// Using it is like not using any scheme for the validator, and thus it will fail
// with schemes enabled.
description: "Placeholder scheme supplied directly should fail",
options: DomainValidatorOptions{WithScheme: true, AllowedSchemes: []string{"https"}},
input: "tinyauth://example.com",
errorFunc: func(t *testing.T, e error) {
assert.ErrorContains(t, e, "input url is missing scheme")
},
},
}
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
v := NewDomainValidator(test.options)
res, err := v.SafeHostname(test.input)
if test.errorFunc != nil {
test.errorFunc(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, test.expected, res)
})
}
}
func TestDomainValidator_Validate(t *testing.T) {
type testCase struct {
description string
options DomainValidatorOptions
expected string
actual string
errorFunc func(t *testing.T, e error)
}
tests := []testCase{
{
description: "Invalid expected domain fails checks",
expected: "foo:foo",
actual: "bar.com",
errorFunc: func(t *testing.T, e error) {
assert.ErrorContains(t, e, "failed to parse input url:")
},
},
{
description: "Invalid check domain fails checks",
expected: "example.com",
actual: "foo:foo",
errorFunc: func(t *testing.T, e error) {
assert.ErrorContains(t, e, "failed to parse input url:")
},
},
{
description: "Valid domains with non-matching schemes should fail",
options: DomainValidatorOptions{WithScheme: true, AllowedSchemes: []string{"https", "http"}},
expected: "https://example.com",
actual: "http://example.com",
errorFunc: func(t *testing.T, e error) {
assert.ErrorIs(t, e, ErrSchemeMismatch)
},
},
{
description: "Valid domains with matching schemes should pass",
options: DomainValidatorOptions{WithScheme: true, AllowedSchemes: []string{"https", "http"}},
expected: "https://example.com",
actual: "https://example.com",
},
{
description: "Port validation without ports and schemes disabled should fail",
options: DomainValidatorOptions{WithPort: true},
expected: "example.com",
actual: "example.com",
errorFunc: func(t *testing.T, e error) {
assert.ErrorContains(t, e, "port validation is enabled but port is missing in input url and schemes are not enabled")
},
},
{
description: "Port validation with no port and http should pass",
options: DomainValidatorOptions{WithPort: true, WithScheme: true, AllowedSchemes: []string{"http"}},
expected: "http://example.com",
actual: "http://example.com",
},
{
description: "Port validation with no port and https should pass",
options: DomainValidatorOptions{WithPort: true, WithScheme: true, AllowedSchemes: []string{"https"}},
expected: "https://example.com",
actual: "https://example.com",
},
{
description: "Port validation with port and no scheme should pass with same port",
options: DomainValidatorOptions{WithPort: true},
expected: "example.com:8080",
actual: "example.com:8080",
},
{
description: "Domains with unknown scheme and port enabled but no port should fail",
options: DomainValidatorOptions{WithPort: true, WithScheme: true},
expected: "ssh://example.com:22",
actual: "ssh://example.com",
errorFunc: func(t *testing.T, e error) {
assert.ErrorContains(t, e, "failed to get effective port for url")
},
},
{
description: "Domains with unknown scheme and port enabled but no port should fail, reverse",
options: DomainValidatorOptions{WithPort: true, WithScheme: true},
expected: "ssh://example.com",
actual: "ssh://example.com:22",
errorFunc: func(t *testing.T, e error) {
assert.ErrorContains(t, e, "failed to get effective port for url")
},
},
{
description: "Port validation with port and no scheme should fail with different port",
options: DomainValidatorOptions{WithPort: true},
expected: "example.com:8080",
actual: "example.com:8081",
errorFunc: func(t *testing.T, e error) {
assert.ErrorIs(t, e, ErrPortMismatch)
},
},
{
description: "Failure to format expected domain should fail",
expected: "ab--cd.example.com",
actual: "example.com",
errorFunc: func(t *testing.T, e error) {
assert.ErrorContains(t, e, "idna: invalid label")
},
},
{
description: "Failure to format check domain should fail",
expected: "example.com",
actual: "ab--cd.example.com",
errorFunc: func(t *testing.T, e error) {
assert.ErrorContains(t, e, "idna: invalid label")
},
},
{
description: "Valid domains with matching schemes and ports should pass",
options: DomainValidatorOptions{WithScheme: true, AllowedSchemes: []string{"https", "http"}, WithPort: true},
expected: "https://example.com:8080",
actual: "https://example.com:8080",
},
{
description: "Valid domains with matching schemes should pass",
options: DomainValidatorOptions{WithScheme: true, AllowedSchemes: []string{"https", "http"}},
expected: "https://example.com",
actual: "https://example.com",
},
{
description: "Valid domains with matching ports should pass",
options: DomainValidatorOptions{WithPort: true},
expected: "example.com:8080",
actual: "example.com:8080",
},
{
description: "Valid domains without ports or schemes should pass",
actual: "example.com",
expected: "example.com",
},
{
description: "Unicode valid domains should pass",
expected: "xn--bcher-kva.example.com",
actual: "bücher.example.com",
},
{
description: "Unicode valid domains should pass (reverse)",
expected: "bücher.example.com",
actual: "xn--bcher-kva.example.com",
},
{
description: "Non matching hostnames should fail",
expected: "example.com",
actual: "foo.com",
errorFunc: func(t *testing.T, e error) {
assert.ErrorIs(t, e, ErrHostnameMismatch)
},
},
}
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
v := NewDomainValidator(test.options)
err := v.Validate(test.expected, test.actual)
if test.errorFunc != nil {
test.errorFunc(t, err)
return
}
require.NoError(t, err)
})
}
}