mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-02-22 17:02:01 +00:00
Compare commits
58 Commits
feat/forwa
...
v5.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22c4c262ea | ||
|
|
baf4798665 | ||
|
|
bea680edec | ||
|
|
3eea68ae0c | ||
|
|
f08d8593ea | ||
|
|
fa1c5292f9 | ||
|
|
ce25f9561f | ||
|
|
f24595b24e | ||
|
|
285edba88c | ||
|
|
51d95fa455 | ||
|
|
fd16f91011 | ||
|
|
fb671139cd | ||
|
|
7ca79d4532 | ||
|
|
9d2d08a537 | ||
|
|
15ee55ca61 | ||
|
|
eff5fc8b71 | ||
|
|
671343f677 | ||
|
|
252ba10f48 | ||
|
|
6431afb7d1 | ||
|
|
f9b221778f | ||
|
|
7ed6174140 | ||
|
|
bbf31be5ae | ||
|
|
402dfa727b | ||
|
|
d67c3ab8a4 | ||
|
|
87e2b52a04 | ||
|
|
f36b62561a | ||
|
|
d2a146ead0 | ||
|
|
4926e53409 | ||
|
|
c0dd432029 | ||
|
|
a0ef7b80d8 | ||
|
|
53bd413046 | ||
|
|
ba2d732415 | ||
|
|
0025485e38 | ||
|
|
3cba90f99c | ||
|
|
a842e40903 | ||
|
|
e3f92ce4fc | ||
|
|
454612226b | ||
|
|
0aa8037edc | ||
|
|
8872e68589 | ||
|
|
1ffb838c0f | ||
|
|
e3c98faf36 | ||
|
|
1dc83c835c | ||
|
|
23987aade8 | ||
|
|
9f52d13028 | ||
|
|
e7bd64d7a3 | ||
|
|
721f302c0b | ||
|
|
f1e2b55cd1 | ||
|
|
f564032a11 | ||
|
|
1ec1f82dbd | ||
|
|
7e17a4ad86 | ||
|
|
2dc047d9b7 | ||
|
|
974f2a67f0 | ||
|
|
3c6bd44906 | ||
|
|
afddb2c353 | ||
|
|
9a3fecd565 | ||
|
|
986ac88e14 | ||
|
|
b159f44729 | ||
|
|
43487d44f7 |
29
.env.example
29
.env.example
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
# The base URL where Tinyauth is accessible
|
# The base URL where Tinyauth is accessible
|
||||||
TINYAUTH_APPURL="https://auth.example.com"
|
TINYAUTH_APPURL="https://auth.example.com"
|
||||||
# Log level: trace, debug, info, warn, error
|
|
||||||
TINYAUTH_LOGLEVEL="info"
|
|
||||||
# Directory for static resources
|
# Directory for static resources
|
||||||
TINYAUTH_RESOURCESDIR="/data/resources"
|
TINYAUTH_RESOURCESDIR="/data/resources"
|
||||||
# Path to SQLite database file
|
# Path to SQLite database file
|
||||||
@@ -14,8 +12,21 @@ TINYAUTH_DISABLEANALYTICS="false"
|
|||||||
TINYAUTH_DISABLERESOURCES="false"
|
TINYAUTH_DISABLERESOURCES="false"
|
||||||
# Disable UI warning messages
|
# Disable UI warning messages
|
||||||
TINYAUTH_DISABLEUIWARNINGS="false"
|
TINYAUTH_DISABLEUIWARNINGS="false"
|
||||||
|
|
||||||
|
# Logging Configuration
|
||||||
|
|
||||||
|
# Log level: trace, debug, info, warn, error
|
||||||
|
TINYAUTH_LOG_LEVEL="info"
|
||||||
# Enable JSON formatted logs
|
# Enable JSON formatted logs
|
||||||
TINYAUTH_LOGJSON="false"
|
TINYAUTH_LOG_JSON="false"
|
||||||
|
# Specific Log stream configurations
|
||||||
|
# APP and HTTP log streams are enabled by default, and use the global log level unless overridden
|
||||||
|
TINYAUTH_LOG_STREAMS_APP_ENABLED="true"
|
||||||
|
TINYAUTH_LOG_STREAMS_APP_LEVEL="info"
|
||||||
|
TINYAUTH_LOG_STREAMS_HTTP_ENABLED="true"
|
||||||
|
TINYAUTH_LOG_STREAMS_HTTP_LEVEL="info"
|
||||||
|
TINYAUTH_LOG_STREAMS_AUDIT_ENABLED="false"
|
||||||
|
TINYAUTH_LOG_STREAMS_AUDIT_LEVEL="info"
|
||||||
|
|
||||||
# Server Configuration
|
# Server Configuration
|
||||||
|
|
||||||
@@ -33,15 +44,17 @@ TINYAUTH_SERVER_TRUSTEDPROXIES=""
|
|||||||
# Format: username:bcrypt_hash (use bcrypt to generate hash)
|
# Format: username:bcrypt_hash (use bcrypt to generate hash)
|
||||||
TINYAUTH_AUTH_USERS="admin:$2a$10$example_bcrypt_hash_here"
|
TINYAUTH_AUTH_USERS="admin:$2a$10$example_bcrypt_hash_here"
|
||||||
# Path to external users file (optional)
|
# Path to external users file (optional)
|
||||||
TINYAUTH_USERSFILE=""
|
TINYAUTH_AUTH_USERSFILE=""
|
||||||
# Enable secure cookies (requires HTTPS)
|
# Enable secure cookies (requires HTTPS)
|
||||||
TINYAUTH_SECURECOOKIE="true"
|
TINYAUTH_AUTH_SECURECOOKIE="true"
|
||||||
# Session expiry in seconds (7200 = 2 hours)
|
# Session expiry in seconds (7200 = 2 hours)
|
||||||
TINYAUTH_SESSIONEXPIRY="7200"
|
TINYAUTH_AUTH_SESSIONEXPIRY="7200"
|
||||||
|
# Session maximum lifetime in seconds (0 = unlimited)
|
||||||
|
TINYAUTH_AUTH_SESSIONMAXLIFETIME="0"
|
||||||
# Login timeout in seconds (300 = 5 minutes)
|
# Login timeout in seconds (300 = 5 minutes)
|
||||||
TINYAUTH_LOGINTIMEOUT="300"
|
TINYAUTH_AUTH_LOGINTIMEOUT="300"
|
||||||
# Maximum login retries before lockout
|
# Maximum login retries before lockout
|
||||||
TINYAUTH_LOGINMAXRETRIES="5"
|
TINYAUTH_AUTH_LOGINMAXRETRIES="5"
|
||||||
|
|
||||||
# OAuth Configuration
|
# OAuth Configuration
|
||||||
|
|
||||||
|
|||||||
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@@ -18,7 +18,16 @@ jobs:
|
|||||||
- name: Setup go
|
- name: Setup go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "^1.23.2"
|
go-version: "^1.24.0"
|
||||||
|
|
||||||
|
- name: Initialize submodules
|
||||||
|
run: |
|
||||||
|
git submodule init
|
||||||
|
git submodule update
|
||||||
|
|
||||||
|
- name: Apply patches
|
||||||
|
run: |
|
||||||
|
git apply --directory paerser/ patches/nested_maps.diff
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
62
.github/workflows/nightly.yml
vendored
62
.github/workflows/nightly.yml
vendored
@@ -61,7 +61,16 @@ jobs:
|
|||||||
- name: Install go
|
- name: Install go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "^1.23.2"
|
go-version: "^1.24.0"
|
||||||
|
|
||||||
|
- name: Initialize submodules
|
||||||
|
run: |
|
||||||
|
git submodule init
|
||||||
|
git submodule update
|
||||||
|
|
||||||
|
- name: Apply patches
|
||||||
|
run: |
|
||||||
|
git apply --directory paerser/ patches/nested_maps.diff
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -80,7 +89,7 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
cp -r frontend/dist internal/assets/dist
|
cp -r frontend/dist internal/assets/dist
|
||||||
go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 ./cmd/tinyauth
|
go build -ldflags "-s -w -X github.com/steveiliop56/tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/steveiliop56/tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/steveiliop56/tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 ./cmd/tinyauth
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
|
|
||||||
@@ -107,7 +116,16 @@ jobs:
|
|||||||
- name: Install go
|
- name: Install go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "^1.23.2"
|
go-version: "^1.24.0"
|
||||||
|
|
||||||
|
- name: Initialize submodules
|
||||||
|
run: |
|
||||||
|
git submodule init
|
||||||
|
git submodule update
|
||||||
|
|
||||||
|
- name: Apply patches
|
||||||
|
run: |
|
||||||
|
git apply --directory paerser/ patches/nested_maps.diff
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -126,7 +144,7 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
cp -r frontend/dist internal/assets/dist
|
cp -r frontend/dist internal/assets/dist
|
||||||
go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 ./cmd/tinyauth
|
go build -ldflags "-s -w -X github.com/steveiliop56/tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/steveiliop56/tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/steveiliop56/tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 ./cmd/tinyauth
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
|
|
||||||
@@ -147,6 +165,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: nightly
|
ref: nightly
|
||||||
|
|
||||||
|
- name: Initialize submodules
|
||||||
|
run: |
|
||||||
|
git submodule init
|
||||||
|
git submodule update
|
||||||
|
|
||||||
|
- name: Apply patches
|
||||||
|
run: |
|
||||||
|
git apply --directory paerser/ patches/nested_maps.diff
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
@@ -205,6 +232,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: nightly
|
ref: nightly
|
||||||
|
|
||||||
|
- name: Initialize submodules
|
||||||
|
run: |
|
||||||
|
git submodule init
|
||||||
|
git submodule update
|
||||||
|
|
||||||
|
- name: Apply patches
|
||||||
|
run: |
|
||||||
|
git apply --directory paerser/ patches/nested_maps.diff
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
@@ -263,6 +299,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: nightly
|
ref: nightly
|
||||||
|
|
||||||
|
- name: Initialize submodules
|
||||||
|
run: |
|
||||||
|
git submodule init
|
||||||
|
git submodule update
|
||||||
|
|
||||||
|
- name: Apply patches
|
||||||
|
run: |
|
||||||
|
git apply --directory paerser/ patches/nested_maps.diff
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
@@ -321,6 +366,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: nightly
|
ref: nightly
|
||||||
|
|
||||||
|
- name: Initialize submodules
|
||||||
|
run: |
|
||||||
|
git submodule init
|
||||||
|
git submodule update
|
||||||
|
|
||||||
|
- name: Apply patches
|
||||||
|
run: |
|
||||||
|
git apply --directory paerser/ patches/nested_maps.diff
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
|
|||||||
62
.github/workflows/release.yml
vendored
62
.github/workflows/release.yml
vendored
@@ -39,7 +39,16 @@ jobs:
|
|||||||
- name: Install go
|
- name: Install go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "^1.23.2"
|
go-version: "^1.24.0"
|
||||||
|
|
||||||
|
- name: Initialize submodules
|
||||||
|
run: |
|
||||||
|
git submodule init
|
||||||
|
git submodule update
|
||||||
|
|
||||||
|
- name: Apply patches
|
||||||
|
run: |
|
||||||
|
git apply --directory paerser/ patches/nested_maps.diff
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -58,7 +67,7 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
cp -r frontend/dist internal/assets/dist
|
cp -r frontend/dist internal/assets/dist
|
||||||
go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 ./cmd/tinyauth
|
go build -ldflags "-s -w -X github.com/steveiliop56/tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/steveiliop56/tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/steveiliop56/tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 ./cmd/tinyauth
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
|
|
||||||
@@ -82,7 +91,16 @@ jobs:
|
|||||||
- name: Install go
|
- name: Install go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "^1.23.2"
|
go-version: "^1.24.0"
|
||||||
|
|
||||||
|
- name: Initialize submodules
|
||||||
|
run: |
|
||||||
|
git submodule init
|
||||||
|
git submodule update
|
||||||
|
|
||||||
|
- name: Apply patches
|
||||||
|
run: |
|
||||||
|
git apply --directory paerser/ patches/nested_maps.diff
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -101,7 +119,7 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
cp -r frontend/dist internal/assets/dist
|
cp -r frontend/dist internal/assets/dist
|
||||||
go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 ./cmd/tinyauth
|
go build -ldflags "-s -w -X github.com/steveiliop56/tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/steveiliop56/tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/steveiliop56/tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 ./cmd/tinyauth
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
|
|
||||||
@@ -119,6 +137,15 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Initialize submodules
|
||||||
|
run: |
|
||||||
|
git submodule init
|
||||||
|
git submodule update
|
||||||
|
|
||||||
|
- name: Apply patches
|
||||||
|
run: |
|
||||||
|
git apply --directory paerser/ patches/nested_maps.diff
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
@@ -174,6 +201,15 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Initialize submodules
|
||||||
|
run: |
|
||||||
|
git submodule init
|
||||||
|
git submodule update
|
||||||
|
|
||||||
|
- name: Apply patches
|
||||||
|
run: |
|
||||||
|
git apply --directory paerser/ patches/nested_maps.diff
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
@@ -229,6 +265,15 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Initialize submodules
|
||||||
|
run: |
|
||||||
|
git submodule init
|
||||||
|
git submodule update
|
||||||
|
|
||||||
|
- name: Apply patches
|
||||||
|
run: |
|
||||||
|
git apply --directory paerser/ patches/nested_maps.diff
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
@@ -284,6 +329,15 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Initialize submodules
|
||||||
|
run: |
|
||||||
|
git submodule init
|
||||||
|
git submodule update
|
||||||
|
|
||||||
|
- name: Apply patches
|
||||||
|
run: |
|
||||||
|
git apply --directory paerser/ patches/nested_maps.diff
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
|
|||||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
# binaries
|
# binaries
|
||||||
/tinyauth
|
/tinyauth
|
||||||
|
/tinyauth-arm64
|
||||||
|
/tinyauth-amd64
|
||||||
|
|
||||||
# test docker compose
|
# test docker compose
|
||||||
/docker-compose.test*
|
/docker-compose.test*
|
||||||
@@ -22,9 +24,6 @@
|
|||||||
# tmp directory
|
# tmp directory
|
||||||
/tmp
|
/tmp
|
||||||
|
|
||||||
# version files
|
|
||||||
/internal/assets/version
|
|
||||||
|
|
||||||
# data directory
|
# data directory
|
||||||
/data
|
/data
|
||||||
|
|
||||||
@@ -36,4 +35,7 @@
|
|||||||
/resources
|
/resources
|
||||||
|
|
||||||
# debug files
|
# debug files
|
||||||
__debug_*
|
__debug_*
|
||||||
|
|
||||||
|
# infisical
|
||||||
|
/.infisical.json
|
||||||
|
|||||||
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[submodule "paerser"]
|
||||||
|
path = paerser
|
||||||
|
url = https://github.com/traefik/paerser
|
||||||
|
ignore = all
|
||||||
13
.zed/debug.json
Normal file
13
.zed/debug.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"label": "Attach to remote Delve",
|
||||||
|
"adapter": "Delve",
|
||||||
|
"mode": "remote",
|
||||||
|
"remotePath": "/tinyauth",
|
||||||
|
"request": "attach",
|
||||||
|
"tcp_connection": {
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 4000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -5,7 +5,7 @@ Contributing is relatively easy, you just need to follow the steps below and you
|
|||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Bun
|
- Bun
|
||||||
- Golang v1.23.2 and above
|
- Golang 1.24.0+
|
||||||
- Git
|
- Git
|
||||||
- Docker
|
- Docker
|
||||||
|
|
||||||
@@ -18,12 +18,21 @@ git clone https://github.com/steveiliop56/tinyauth
|
|||||||
cd tinyauth
|
cd tinyauth
|
||||||
```
|
```
|
||||||
|
|
||||||
## Install requirements
|
## Initialize submodules
|
||||||
|
|
||||||
Although you will not need the requirements in your machine since the development will happen in docker, I still recommend to install them because this way you will not have import errors. To install the go requirements run:
|
The project uses Git submodules for some dependencies, so you need to initialize them with:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go mod tidy
|
git submodule init
|
||||||
|
git submodule update
|
||||||
|
```
|
||||||
|
|
||||||
|
## Install requirements
|
||||||
|
|
||||||
|
Although you will not need the requirements in your machine since the development will happen in Docker, I still recommend to install them because this way you will not have import errors. To install the Go requirements run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go mod download
|
||||||
```
|
```
|
||||||
|
|
||||||
You also need to download the frontend dependencies, this can be done like so:
|
You also need to download the frontend dependencies, this can be done like so:
|
||||||
@@ -33,13 +42,21 @@ cd frontend/
|
|||||||
bun install
|
bun install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Apply patches
|
||||||
|
|
||||||
|
Some of the dependencies need to be patched in order to work correctly with the project, you can apply the patches by running:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git apply --directory paerser/ patches/nested_maps.diff
|
||||||
|
```
|
||||||
|
|
||||||
## Create your `.env` file
|
## Create your `.env` file
|
||||||
|
|
||||||
In order to configure the app you need to create an environment file, this can be done by copying the `.env.example` file to `.env` and modifying the environment variables to suit your needs.
|
In order to configure the app you need to create an environment file, this can be done by copying the `.env.example` file to `.env` and modifying the environment variables to suit your needs.
|
||||||
|
|
||||||
## Developing
|
## Developing
|
||||||
|
|
||||||
I have designed the development workflow to be entirely in docker, this is because it will directly work with traefik and you will not need to do any building in your host machine. The recommended development setup is to have a subdomain pointing to your machine like this:
|
I have designed the development workflow to be entirely in Docker, this is because it will directly work with Traefik and you will not need to do any building in your host machine. The recommended development setup is to have a subdomain pointing to your machine like this:
|
||||||
|
|
||||||
```
|
```
|
||||||
*.dev.example.com -> 127.0.0.1
|
*.dev.example.com -> 127.0.0.1
|
||||||
@@ -49,7 +66,7 @@ dev.example.com -> 127.0.0.1
|
|||||||
> [!TIP]
|
> [!TIP]
|
||||||
> You can use [sslip.io](https://sslip.io) as a domain if you don't have one to develop with.
|
> You can use [sslip.io](https://sslip.io) as a domain if you don't have one to develop with.
|
||||||
|
|
||||||
Then you can just make sure the domains are correct in the development docker compose file and run:
|
Then you can just make sure the domains are correct in the development Docker compose file and run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker compose -f docker-compose.dev.yml up --build
|
docker compose -f docker-compose.dev.yml up --build
|
||||||
|
|||||||
19
Dockerfile
19
Dockerfile
@@ -1,5 +1,5 @@
|
|||||||
# Site builder
|
# Site builder
|
||||||
FROM oven/bun:1.3.5-alpine AS frontend-builder
|
FROM oven/bun:1.3.8-alpine AS frontend-builder
|
||||||
|
|
||||||
WORKDIR /frontend
|
WORKDIR /frontend
|
||||||
|
|
||||||
@@ -28,6 +28,8 @@ ARG BUILD_TIMESTAMP
|
|||||||
|
|
||||||
WORKDIR /tinyauth
|
WORKDIR /tinyauth
|
||||||
|
|
||||||
|
COPY ./paerser ./paerser
|
||||||
|
|
||||||
COPY go.mod ./
|
COPY go.mod ./
|
||||||
COPY go.sum ./
|
COPY go.sum ./
|
||||||
|
|
||||||
@@ -37,8 +39,11 @@ COPY ./cmd ./cmd
|
|||||||
COPY ./internal ./internal
|
COPY ./internal ./internal
|
||||||
COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
|
COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w -X tinyauth/internal/config.Version=${VERSION} -X tinyauth/internal/config.CommitHash=${COMMIT_HASH} -X tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth
|
RUN CGO_ENABLED=0 go build -ldflags "-s -w \
|
||||||
|
-X github.com/steveiliop56/tinyauth/internal/config.Version=${VERSION} \
|
||||||
|
-X github.com/steveiliop56/tinyauth/internal/config.CommitHash=${COMMIT_HASH} \
|
||||||
|
-X github.com/steveiliop56/tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth
|
||||||
|
|
||||||
# Runner
|
# Runner
|
||||||
FROM alpine:3.23 AS runner
|
FROM alpine:3.23 AS runner
|
||||||
|
|
||||||
@@ -52,14 +57,12 @@ EXPOSE 3000
|
|||||||
|
|
||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
|
|
||||||
ENV DATABASEPATH=/data/tinyauth.db
|
ENV TINYAUTH_DATABASEPATH=/data/tinyauth.db
|
||||||
|
|
||||||
ENV RESOURCESDIR=/data/resources
|
ENV TINYAUTH_RESOURCESDIR=/data/resources
|
||||||
|
|
||||||
ENV GIN_MODE=release
|
|
||||||
|
|
||||||
ENV PATH=$PATH:/tinyauth
|
ENV PATH=$PATH:/tinyauth
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 CMD ["tinyauth", "healthcheck"]
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 CMD ["tinyauth", "healthcheck"]
|
||||||
|
|
||||||
ENTRYPOINT ["tinyauth"]
|
ENTRYPOINT ["tinyauth"]
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ FROM golang:1.25-alpine3.21
|
|||||||
|
|
||||||
WORKDIR /tinyauth
|
WORKDIR /tinyauth
|
||||||
|
|
||||||
|
COPY ./paerser ./paerser
|
||||||
|
|
||||||
COPY go.mod ./
|
COPY go.mod ./
|
||||||
COPY go.sum ./
|
COPY go.sum ./
|
||||||
|
|
||||||
@@ -20,4 +22,4 @@ ENV TINYAUTH_DATABASEPATH=/data/tinyauth.db
|
|||||||
|
|
||||||
ENV TINYAUTH_RESOURCESDIR=/data/resources
|
ENV TINYAUTH_RESOURCESDIR=/data/resources
|
||||||
|
|
||||||
ENTRYPOINT ["air", "-c", "air.toml"]
|
ENTRYPOINT ["air", "-c", "air.toml"]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Site builder
|
# Site builder
|
||||||
FROM oven/bun:1.3.5-alpine AS frontend-builder
|
FROM oven/bun:1.3.8-alpine AS frontend-builder
|
||||||
|
|
||||||
WORKDIR /frontend
|
WORKDIR /frontend
|
||||||
|
|
||||||
@@ -28,6 +28,8 @@ ARG BUILD_TIMESTAMP
|
|||||||
|
|
||||||
WORKDIR /tinyauth
|
WORKDIR /tinyauth
|
||||||
|
|
||||||
|
COPY ./paerser ./paerser
|
||||||
|
|
||||||
COPY go.mod ./
|
COPY go.mod ./
|
||||||
COPY go.sum ./
|
COPY go.sum ./
|
||||||
|
|
||||||
@@ -39,8 +41,11 @@ COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
|
|||||||
|
|
||||||
RUN mkdir -p data
|
RUN mkdir -p data
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w -X tinyauth/internal/config.Version=${VERSION} -X tinyauth/internal/config.CommitHash=${COMMIT_HASH} -X tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth
|
RUN CGO_ENABLED=0 go build -ldflags "-s -w \
|
||||||
|
-X github.com/steveiliop56/tinyauth/internal/config.Version=${VERSION} \
|
||||||
|
-X github.com/steveiliop56/tinyauth/internal/config.CommitHash=${COMMIT_HASH} \
|
||||||
|
-X github.com/steveiliop56/tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth
|
||||||
|
|
||||||
# Runner
|
# Runner
|
||||||
FROM gcr.io/distroless/static-debian12:latest AS runner
|
FROM gcr.io/distroless/static-debian12:latest AS runner
|
||||||
|
|
||||||
@@ -59,10 +64,8 @@ ENV TINYAUTH_DATABASEPATH=/data/tinyauth.db
|
|||||||
|
|
||||||
ENV TINYAUTH_RESOURCESDIR=/data/resources
|
ENV TINYAUTH_RESOURCESDIR=/data/resources
|
||||||
|
|
||||||
ENV GIN_MODE=release
|
|
||||||
|
|
||||||
ENV PATH=$PATH:/tinyauth
|
ENV PATH=$PATH:/tinyauth
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 CMD ["tinyauth", "healthcheck"]
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 CMD ["tinyauth", "healthcheck"]
|
||||||
|
|
||||||
ENTRYPOINT ["tinyauth"]
|
ENTRYPOINT ["tinyauth"]
|
||||||
|
|||||||
81
Makefile
Normal file
81
Makefile
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# Go specific stuff
|
||||||
|
CGO_ENABLED := 0
|
||||||
|
GOOS := $(shell go env GOOS)
|
||||||
|
GOARCH := $(shell go env GOARCH)
|
||||||
|
|
||||||
|
# Build out
|
||||||
|
TAG_NAME := $(shell git describe --abbrev=0 --exact-match 2> /dev/null || echo "main")
|
||||||
|
COMMIT_HASH := $(shell git rev-parse HEAD)
|
||||||
|
BUILD_TIMESTAMP := $(shell date '+%Y-%m-%dT%H:%M:%S')
|
||||||
|
BIN_NAME := tinyauth-$(GOARCH)
|
||||||
|
|
||||||
|
# Development vars
|
||||||
|
DEV_COMPOSE := $(shell test -f "docker-compose.test.yml" && echo "docker-compose.test.yml" || echo "docker-compose.yml" )
|
||||||
|
PROD_COMPOSE := $(shell test -f "docker-compose.test.prod.yml" && echo "docker-compose.test.prod.yml" || echo "docker-compose.example.yml" )
|
||||||
|
|
||||||
|
# Deps
|
||||||
|
deps:
|
||||||
|
bun install --cwd frontend
|
||||||
|
go mod download
|
||||||
|
|
||||||
|
# Clean data
|
||||||
|
clean-data:
|
||||||
|
rm -rf data/
|
||||||
|
|
||||||
|
# Clean web UI build
|
||||||
|
clean-webui:
|
||||||
|
rm -rf internal/assets/dist
|
||||||
|
rm -rf frontend/dist
|
||||||
|
|
||||||
|
# Build the web UI
|
||||||
|
webui: clean-webui
|
||||||
|
bun run --cwd frontend build
|
||||||
|
cp -r frontend/dist internal/assets
|
||||||
|
|
||||||
|
# Build the binary
|
||||||
|
binary: webui
|
||||||
|
CGO_ENABLED=$(CGO_ENABLED) go build -ldflags "-s -w \
|
||||||
|
-X github.com/steveiliop56/tinyauth/internal/config.Version=${TAG_NAME} \
|
||||||
|
-X github.com/steveiliop56/tinyauth/internal/config.CommitHash=${COMMIT_HASH} \
|
||||||
|
-X github.com/steveiliop56/tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" \
|
||||||
|
-o ${BIN_NAME} ./cmd/tinyauth
|
||||||
|
|
||||||
|
# Build for amd64
|
||||||
|
binary-linux-amd64:
|
||||||
|
export BIN_NAME=tinyauth-amd64
|
||||||
|
export GOARCH=amd64
|
||||||
|
export GOOS=linux
|
||||||
|
$(MAKE) binary
|
||||||
|
|
||||||
|
# Build for arm64
|
||||||
|
binary-linux-arm64:
|
||||||
|
export BIN_NAME=tinyauth-arm64
|
||||||
|
export GOARCH=arm64
|
||||||
|
export GOOS=linux
|
||||||
|
$(MAKE) binary
|
||||||
|
|
||||||
|
# Go test
|
||||||
|
.PHONY: test
|
||||||
|
test:
|
||||||
|
go test -v ./...
|
||||||
|
|
||||||
|
# Development
|
||||||
|
develop:
|
||||||
|
docker compose -f $(DEV_COMPOSE) up --force-recreate --pull=always --remove-orphans --build
|
||||||
|
|
||||||
|
# Development - Infisical
|
||||||
|
develop-infisical:
|
||||||
|
infisical run --env=dev -- docker compose -f $(DEV_COMPOSE) up --force-recreate --pull=always --remove-orphans --build
|
||||||
|
|
||||||
|
# Production
|
||||||
|
prod:
|
||||||
|
docker compose -f $(PROD_COMPOSE) up --force-recreate --pull=always --remove-orphans
|
||||||
|
|
||||||
|
# Production - Infisical
|
||||||
|
prod-infisical:
|
||||||
|
infisical run --env=dev -- docker compose -f $(PROD_COMPOSE) up --force-recreate --pull=always --remove-orphans
|
||||||
|
|
||||||
|
# SQL
|
||||||
|
.PHONY: sql
|
||||||
|
sql:
|
||||||
|
sqlc generate
|
||||||
@@ -55,7 +55,7 @@ Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You ma
|
|||||||
|
|
||||||
A big thank you to the following people for providing me with more coffee:
|
A big thank you to the following people for providing me with more coffee:
|
||||||
|
|
||||||
<!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https://github.com/erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a> <a href="https://github.com/nicotsx"><img src="https://github.com/nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a> <a href="https://github.com/SimpleHomelab"><img src="https://github.com/SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a> <a href="https://github.com/jmadden91"><img src="https://github.com/jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a> <a href="https://github.com/tribor"><img src="https://github.com/tribor.png" width="64px" alt="User avatar: tribor" /></a> <a href="https://github.com/eliasbenb"><img src="https://github.com/eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a> <a href="https://github.com/afunworm"><img src="https://github.com/afunworm.png" width="64px" alt="User avatar: afunworm" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="64px" alt="User avatar: chip-well" /></a> <a href="https://github.com/Lancelot-Enguerrand"><img src="https://github.com/Lancelot-Enguerrand.png" width="64px" alt="User avatar: Lancelot-Enguerrand" /></a> <a href="https://github.com/allgoewer"><img src="https://github.com/allgoewer.png" width="64px" alt="User avatar: allgoewer" /></a> <!-- sponsors -->
|
<!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https://github.com/erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a> <a href="https://github.com/nicotsx"><img src="https://github.com/nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a> <a href="https://github.com/SimpleHomelab"><img src="https://github.com/SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a> <a href="https://github.com/jmadden91"><img src="https://github.com/jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a> <a href="https://github.com/tribor"><img src="https://github.com/tribor.png" width="64px" alt="User avatar: tribor" /></a> <a href="https://github.com/eliasbenb"><img src="https://github.com/eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a> <a href="https://github.com/afunworm"><img src="https://github.com/afunworm.png" width="64px" alt="User avatar: afunworm" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="64px" alt="User avatar: chip-well" /></a> <a href="https://github.com/Lancelot-Enguerrand"><img src="https://github.com/Lancelot-Enguerrand.png" width="64px" alt="User avatar: Lancelot-Enguerrand" /></a> <a href="https://github.com/allgoewer"><img src="https://github.com/allgoewer.png" width="64px" alt="User avatar: allgoewer" /></a> <a href="https://github.com/NEANC"><img src="https://github.com/NEANC.png" width="64px" alt="User avatar: NEANC" /></a> <a href="https://github.com/algorist-ahmad"><img src="https://github.com/algorist-ahmad.png" width="64px" alt="User avatar: algorist-ahmad" /></a> <!-- sponsors -->
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,10 @@ package main
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/huh"
|
"github.com/charmbracelet/huh"
|
||||||
"github.com/rs/zerolog"
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/traefik/paerser/cli"
|
"github.com/traefik/paerser/cli"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
@@ -43,7 +40,7 @@ func createUserCmd() *cli.Command {
|
|||||||
Configuration: tCfg,
|
Configuration: tCfg,
|
||||||
Resources: loaders,
|
Resources: loaders,
|
||||||
Run: func(_ []string) error {
|
Run: func(_ []string) error {
|
||||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Caller().Logger().Level(zerolog.InfoLevel)
|
tlog.NewSimpleLogger().Init()
|
||||||
|
|
||||||
if tCfg.Interactive {
|
if tCfg.Interactive {
|
||||||
form := huh.NewForm(
|
form := huh.NewForm(
|
||||||
@@ -77,7 +74,7 @@ func createUserCmd() *cli.Command {
|
|||||||
return errors.New("username and password cannot be empty")
|
return errors.New("username and password cannot be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Str("username", tCfg.Username).Msg("Creating user")
|
tlog.App.Info().Str("username", tCfg.Username).Msg("Creating user")
|
||||||
|
|
||||||
passwd, err := bcrypt.GenerateFromPassword([]byte(tCfg.Password), bcrypt.DefaultCost)
|
passwd, err := bcrypt.GenerateFromPassword([]byte(tCfg.Password), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -90,7 +87,7 @@ func createUserCmd() *cli.Command {
|
|||||||
passwdStr = strings.ReplaceAll(passwdStr, "$", "$$")
|
passwdStr = strings.ReplaceAll(passwdStr, "$", "$$")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Str("user", fmt.Sprintf("%s:%s", tCfg.Username, passwdStr)).Msg("User created")
|
tlog.App.Info().Str("user", fmt.Sprintf("%s:%s", tCfg.Username, passwdStr)).Msg("User created")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,15 +5,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/charmbracelet/huh"
|
"github.com/charmbracelet/huh"
|
||||||
"github.com/mdp/qrterminal/v3"
|
"github.com/mdp/qrterminal/v3"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/traefik/paerser/cli"
|
"github.com/traefik/paerser/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -42,7 +40,7 @@ func generateTotpCmd() *cli.Command {
|
|||||||
Configuration: tCfg,
|
Configuration: tCfg,
|
||||||
Resources: loaders,
|
Resources: loaders,
|
||||||
Run: func(_ []string) error {
|
Run: func(_ []string) error {
|
||||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Caller().Logger().Level(zerolog.InfoLevel)
|
tlog.NewSimpleLogger().Init()
|
||||||
|
|
||||||
if tCfg.Interactive {
|
if tCfg.Interactive {
|
||||||
form := huh.NewForm(
|
form := huh.NewForm(
|
||||||
@@ -91,9 +89,9 @@ func generateTotpCmd() *cli.Command {
|
|||||||
|
|
||||||
secret := key.Secret()
|
secret := key.Secret()
|
||||||
|
|
||||||
log.Info().Str("secret", secret).Msg("Generated TOTP secret")
|
tlog.App.Info().Str("secret", secret).Msg("Generated TOTP secret")
|
||||||
|
|
||||||
log.Info().Msg("Generated QR code")
|
tlog.App.Info().Msg("Generated QR code")
|
||||||
|
|
||||||
config := qrterminal.Config{
|
config := qrterminal.Config{
|
||||||
Level: qrterminal.L,
|
Level: qrterminal.L,
|
||||||
@@ -112,7 +110,7 @@ func generateTotpCmd() *cli.Command {
|
|||||||
user.Password = strings.ReplaceAll(user.Password, "$", "$$")
|
user.Password = strings.ReplaceAll(user.Password, "$", "$$")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Str("user", fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TotpSecret)).Msg("Add the totp secret to your authenticator app then use the verify command to ensure everything is working correctly.")
|
tlog.App.Info().Str("user", fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TotpSecret)).Msg("Add the totp secret to your authenticator app then use the verify command to ensure everything is working correctly.")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/traefik/paerser/cli"
|
"github.com/traefik/paerser/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,19 +26,26 @@ func healthcheckCmd() *cli.Command {
|
|||||||
Resources: nil,
|
Resources: nil,
|
||||||
AllowArg: true,
|
AllowArg: true,
|
||||||
Run: func(args []string) error {
|
Run: func(args []string) error {
|
||||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Caller().Logger().Level(zerolog.InfoLevel)
|
tlog.NewSimpleLogger().Init()
|
||||||
|
|
||||||
appUrl := os.Getenv("TINYAUTH_APPURL")
|
appUrl := "http://127.0.0.1:3000"
|
||||||
|
|
||||||
|
srvAddr := os.Getenv("TINYAUTH_SERVER_ADDRESS")
|
||||||
|
srvPort := os.Getenv("TINYAUTH_SERVER_PORT")
|
||||||
|
|
||||||
|
if srvAddr != "" && srvPort != "" {
|
||||||
|
appUrl = fmt.Sprintf("http://%s:%s", srvAddr, srvPort)
|
||||||
|
}
|
||||||
|
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
appUrl = args[0]
|
appUrl = args[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
if appUrl == "" {
|
if appUrl == "" {
|
||||||
return errors.New("TINYAUTH_APPURL is not set and no argument was provided")
|
return errors.New("Could not determine app URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Str("app_url", appUrl).Msg("Performing health check")
|
tlog.App.Info().Str("app_url", appUrl).Msg("Performing health check")
|
||||||
|
|
||||||
client := http.Client{
|
client := http.Client{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
@@ -77,7 +83,7 @@ func healthcheckCmd() *cli.Command {
|
|||||||
return fmt.Errorf("failed to decode response: %w", err)
|
return fmt.Errorf("failed to decode response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Interface("response", healthResp).Msg("Tinyauth is healthy")
|
tlog.App.Info().Interface("response", healthResp).Msg("Tinyauth is healthy")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,22 +2,18 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/bootstrap"
|
"github.com/steveiliop56/tinyauth/internal/bootstrap"
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/loaders"
|
"github.com/steveiliop56/tinyauth/internal/utils/loaders"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/traefik/paerser/cli"
|
"github.com/traefik/paerser/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewTinyauthCmdConfiguration() *config.Config {
|
func NewTinyauthCmdConfiguration() *config.Config {
|
||||||
return &config.Config{
|
return &config.Config{
|
||||||
LogLevel: "info",
|
|
||||||
ResourcesDir: "./resources",
|
ResourcesDir: "./resources",
|
||||||
DatabasePath: "./tinyauth.db",
|
DatabasePath: "./tinyauth.db",
|
||||||
Server: config.ServerConfig{
|
Server: config.ServerConfig{
|
||||||
@@ -25,15 +21,43 @@ func NewTinyauthCmdConfiguration() *config.Config {
|
|||||||
Address: "0.0.0.0",
|
Address: "0.0.0.0",
|
||||||
},
|
},
|
||||||
Auth: config.AuthConfig{
|
Auth: config.AuthConfig{
|
||||||
SessionExpiry: 3600,
|
SessionExpiry: 86400, // 1 day
|
||||||
LoginTimeout: 300,
|
SessionMaxLifetime: 0, // disabled
|
||||||
LoginMaxRetries: 3,
|
LoginTimeout: 300, // 5 minutes
|
||||||
|
LoginMaxRetries: 3,
|
||||||
},
|
},
|
||||||
UI: config.UIConfig{
|
UI: config.UIConfig{
|
||||||
Title: "Tinyauth",
|
Title: "Tinyauth",
|
||||||
ForgotPasswordMessage: "You can change your password by changing the configuration.",
|
ForgotPasswordMessage: "You can change your password by changing the configuration.",
|
||||||
BackgroundImage: "/background.jpg",
|
BackgroundImage: "/background.jpg",
|
||||||
},
|
},
|
||||||
|
Ldap: config.LdapConfig{
|
||||||
|
Insecure: false,
|
||||||
|
SearchFilter: "(uid=%s)",
|
||||||
|
GroupCacheTTL: 900, // 15 minutes
|
||||||
|
},
|
||||||
|
Log: config.LogConfig{
|
||||||
|
Level: "info",
|
||||||
|
Json: false,
|
||||||
|
Streams: config.LogStreams{
|
||||||
|
HTTP: config.LogStreamConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Level: "",
|
||||||
|
},
|
||||||
|
App: config.LogStreamConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Level: "",
|
||||||
|
},
|
||||||
|
Audit: config.LogStreamConfig{
|
||||||
|
Enabled: false,
|
||||||
|
Level: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
OIDC: config.OIDCConfig{
|
||||||
|
PrivateKeyPath: "./tinyauth_oidc_key",
|
||||||
|
PublicKeyPath: "./tinyauth_oidc_key.pub",
|
||||||
|
},
|
||||||
Experimental: config.ExperimentalConfig{
|
Experimental: config.ExperimentalConfig{
|
||||||
ConfigFile: "",
|
ConfigFile: "",
|
||||||
},
|
},
|
||||||
@@ -59,13 +83,23 @@ func main() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmdUser := &cli.Command{
|
||||||
|
Name: "user",
|
||||||
|
Description: "Utilities for creating and verifying Tinyauth users.",
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdTotp := &cli.Command{
|
||||||
|
Name: "totp",
|
||||||
|
Description: "Utilities for creating Tinyauth TOTP users.",
|
||||||
|
}
|
||||||
|
|
||||||
err := cmdTinyauth.AddCommand(versionCmd())
|
err := cmdTinyauth.AddCommand(versionCmd())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add version command")
|
log.Fatal().Err(err).Msg("Failed to add version command")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cmdTinyauth.AddCommand(verifyUserCmd())
|
err = cmdUser.AddCommand(verifyUserCmd())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add verify command")
|
log.Fatal().Err(err).Msg("Failed to add verify command")
|
||||||
@@ -77,18 +111,30 @@ func main() {
|
|||||||
log.Fatal().Err(err).Msg("Failed to add healthcheck command")
|
log.Fatal().Err(err).Msg("Failed to add healthcheck command")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cmdTinyauth.AddCommand(generateTotpCmd())
|
err = cmdTotp.AddCommand(generateTotpCmd())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add generate command")
|
log.Fatal().Err(err).Msg("Failed to add generate command")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cmdTinyauth.AddCommand(createUserCmd())
|
err = cmdUser.AddCommand(createUserCmd())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add create command")
|
log.Fatal().Err(err).Msg("Failed to add create command")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = cmdTinyauth.AddCommand(cmdUser)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Failed to add user command")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cmdTinyauth.AddCommand(cmdTotp)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Failed to add totp command")
|
||||||
|
}
|
||||||
|
|
||||||
err = cli.Execute(cmdTinyauth)
|
err = cli.Execute(cmdTinyauth)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -97,25 +143,14 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runCmd(cfg config.Config) error {
|
func runCmd(cfg config.Config) error {
|
||||||
logLevel, err := zerolog.ParseLevel(strings.ToLower(cfg.LogLevel))
|
logger := tlog.NewLogger(cfg.Log)
|
||||||
|
logger.Init()
|
||||||
|
|
||||||
if err != nil {
|
tlog.App.Info().Str("version", config.Version).Msg("Starting tinyauth")
|
||||||
log.Error().Err(err).Msg("Invalid or missing log level, defaulting to info")
|
|
||||||
} else {
|
|
||||||
zerolog.SetGlobalLevel(logLevel)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Logger = log.With().Caller().Logger()
|
|
||||||
|
|
||||||
if !cfg.LogJSON {
|
|
||||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339})
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Str("version", config.Version).Msg("Starting tinyauth")
|
|
||||||
|
|
||||||
app := bootstrap.NewBootstrapApp(cfg)
|
app := bootstrap.NewBootstrapApp(cfg)
|
||||||
|
|
||||||
err = app.Setup()
|
err := app.Setup()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to bootstrap app: %w", err)
|
return fmt.Errorf("failed to bootstrap app: %w", err)
|
||||||
|
|||||||
@@ -3,15 +3,12 @@ package main
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/charmbracelet/huh"
|
"github.com/charmbracelet/huh"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/traefik/paerser/cli"
|
"github.com/traefik/paerser/cli"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
@@ -47,7 +44,7 @@ func verifyUserCmd() *cli.Command {
|
|||||||
Configuration: tCfg,
|
Configuration: tCfg,
|
||||||
Resources: loaders,
|
Resources: loaders,
|
||||||
Run: func(_ []string) error {
|
Run: func(_ []string) error {
|
||||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Caller().Logger().Level(zerolog.InfoLevel)
|
tlog.NewSimpleLogger().Init()
|
||||||
|
|
||||||
if tCfg.Interactive {
|
if tCfg.Interactive {
|
||||||
form := huh.NewForm(
|
form := huh.NewForm(
|
||||||
@@ -101,9 +98,9 @@ func verifyUserCmd() *cli.Command {
|
|||||||
|
|
||||||
if user.TotpSecret == "" {
|
if user.TotpSecret == "" {
|
||||||
if tCfg.Totp != "" {
|
if tCfg.Totp != "" {
|
||||||
log.Warn().Msg("User does not have TOTP secret")
|
tlog.App.Warn().Msg("User does not have TOTP secret")
|
||||||
}
|
}
|
||||||
log.Info().Msg("User verified")
|
tlog.App.Info().Msg("User verified")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +110,7 @@ func verifyUserCmd() *cli.Command {
|
|||||||
return fmt.Errorf("TOTP code incorrect")
|
return fmt.Errorf("TOTP code incorrect")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Msg("User verified")
|
tlog.App.Info().Msg("User verified")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
# The base URL where Tinyauth is accessible
|
# The base URL where Tinyauth is accessible
|
||||||
appUrl: "https://auth.example.com"
|
appUrl: "https://auth.example.com"
|
||||||
# Log level: trace, debug, info, warn, error
|
|
||||||
logLevel: "info"
|
|
||||||
# Directory for static resources
|
# Directory for static resources
|
||||||
resourcesDir: "./resources"
|
resourcesDir: "./resources"
|
||||||
# Path to SQLite database file
|
# Path to SQLite database file
|
||||||
@@ -14,8 +12,22 @@ disableAnalytics: false
|
|||||||
disableResources: false
|
disableResources: false
|
||||||
# Disable UI warning messages
|
# Disable UI warning messages
|
||||||
disableUIWarnings: false
|
disableUIWarnings: false
|
||||||
# Enable JSON formatted logs
|
|
||||||
logJSON: false
|
# Logging Configuration
|
||||||
|
log:
|
||||||
|
# Log level: trace, debug, info, warn, error
|
||||||
|
level: "info"
|
||||||
|
json: false
|
||||||
|
streams:
|
||||||
|
app:
|
||||||
|
enabled: true
|
||||||
|
level: "warn"
|
||||||
|
http:
|
||||||
|
enabled: true
|
||||||
|
level: "debug"
|
||||||
|
audit:
|
||||||
|
enabled: false
|
||||||
|
level: "info"
|
||||||
|
|
||||||
# Server Configuration
|
# Server Configuration
|
||||||
server:
|
server:
|
||||||
@@ -38,6 +50,8 @@ auth:
|
|||||||
secureCookie: false
|
secureCookie: false
|
||||||
# Session expiry in seconds (3600 = 1 hour)
|
# Session expiry in seconds (3600 = 1 hour)
|
||||||
sessionExpiry: 3600
|
sessionExpiry: 3600
|
||||||
|
# Session maximum lifetime in seconds (0 = unlimited)
|
||||||
|
sessionMaxLifetime: 0
|
||||||
# Login timeout in seconds (300 = 5 minutes)
|
# Login timeout in seconds (300 = 5 minutes)
|
||||||
loginTimeout: 300
|
loginTimeout: 300
|
||||||
# Maximum login retries before lockout
|
# Maximum login retries before lockout
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
image: traefik:v3.3
|
image: traefik:v3.6
|
||||||
command: --api.insecure=true --providers.docker
|
command: --api.insecure=true --providers.docker
|
||||||
ports:
|
ports:
|
||||||
- 80:80
|
- 80:80
|
||||||
@@ -42,7 +42,6 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./internal:/tinyauth/internal
|
- ./internal:/tinyauth/internal
|
||||||
- ./cmd:/tinyauth/cmd
|
- ./cmd:/tinyauth/cmd
|
||||||
- ./main.go:/tinyauth/main.go
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
- ./data:/data
|
- ./data:/data
|
||||||
ports:
|
ports:
|
||||||
@@ -51,3 +50,4 @@ services:
|
|||||||
labels:
|
labels:
|
||||||
traefik.enable: true
|
traefik.enable: true
|
||||||
traefik.http.middlewares.tinyauth.forwardauth.address: http://tinyauth-backend:3000/api/auth/traefik
|
traefik.http.middlewares.tinyauth.forwardauth.address: http://tinyauth-backend:3000/api/auth/traefik
|
||||||
|
traefik.http.middlewares.tinyauth.forwardauth.authResponseHeaders: remote-user, remote-sub, remote-name, remote-email, remote-groups
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
image: traefik:v3.3
|
image: traefik:v3.6
|
||||||
command: --api.insecure=true --providers.docker
|
command: --api.insecure=true --providers.docker
|
||||||
ports:
|
ports:
|
||||||
- 80:80
|
- 80:80
|
||||||
|
|||||||
@@ -12,72 +12,72 @@
|
|||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tanstack/react-query": "^5.90.12",
|
"@tanstack/react-query": "^5.90.20",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"i18next": "^25.7.3",
|
"i18next": "^25.8.3",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
"i18next-resources-to-backend": "^1.2.1",
|
"i18next-resources-to-backend": "^1.2.1",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.563.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.4",
|
||||||
"react-hook-form": "^7.68.0",
|
"react-hook-form": "^7.71.1",
|
||||||
"react-i18next": "^16.5.0",
|
"react-i18next": "^16.5.4",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-router": "^7.11.0",
|
"react-router": "^7.13.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"zod": "^4.2.1",
|
"zod": "^4.3.6",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@tanstack/eslint-plugin-query": "^5.91.2",
|
"@tanstack/eslint-plugin-query": "^5.91.4",
|
||||||
"@types/node": "^25.0.3",
|
"@types/node": "^25.2.0",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.11",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.2",
|
"@vitejs/plugin-react": "^5.1.3",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.26",
|
"eslint-plugin-react-refresh": "^0.5.0",
|
||||||
"globals": "^16.5.0",
|
"globals": "^17.3.0",
|
||||||
"prettier": "3.7.4",
|
"prettier": "3.8.1",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.50.0",
|
"typescript-eslint": "^8.54.0",
|
||||||
"vite": "^7.3.0",
|
"vite": "^7.3.1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
|
||||||
|
|
||||||
"@babel/compat-data": ["@babel/compat-data@7.27.2", "", {}, "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ=="],
|
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
|
||||||
|
|
||||||
"@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
|
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
|
||||||
|
|
||||||
"@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
"@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
|
||||||
|
|
||||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
|
||||||
|
|
||||||
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||||
|
|
||||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
|
||||||
|
|
||||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
|
||||||
|
|
||||||
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
|
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
|
||||||
|
|
||||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||||
|
|
||||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||||
|
|
||||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||||
|
|
||||||
"@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
"@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
|
||||||
|
|
||||||
"@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
|
"@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
|
||||||
|
|
||||||
@@ -87,11 +87,11 @@
|
|||||||
|
|
||||||
"@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
"@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
||||||
|
|
||||||
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
||||||
|
|
||||||
"@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
|
||||||
|
|
||||||
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||||
|
|
||||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="],
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="],
|
||||||
|
|
||||||
@@ -193,12 +193,6 @@
|
|||||||
|
|
||||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
|
||||||
|
|
||||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
|
||||||
|
|
||||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
|
||||||
|
|
||||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
|
||||||
|
|
||||||
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
||||||
|
|
||||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
||||||
@@ -263,7 +257,7 @@
|
|||||||
|
|
||||||
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
||||||
|
|
||||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="],
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.2", "", {}, "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.46.2", "", { "os": "android", "cpu": "arm" }, "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA=="],
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.46.2", "", { "os": "android", "cpu": "arm" }, "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA=="],
|
||||||
|
|
||||||
@@ -337,11 +331,11 @@
|
|||||||
|
|
||||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="],
|
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="],
|
||||||
|
|
||||||
"@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.91.2", "", { "dependencies": { "@typescript-eslint/utils": "^8.44.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-UPeWKl/Acu1IuuHJlsN+eITUHqAaa9/04geHHPedY8siVarSaWprY0SVMKrkpKfk5ehRT7+/MZ5QwWuEtkWrFw=="],
|
"@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.91.4", "", { "dependencies": { "@typescript-eslint/utils": "^8.48.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-8a+GAeR7oxJ5laNyYBQ6miPK09Hi18o5Oie/jx8zioXODv/AUFLZQecKabPdpQSLmuDXEBPKFh+W5DKbWlahjQ=="],
|
||||||
|
|
||||||
"@tanstack/query-core": ["@tanstack/query-core@5.90.12", "", {}, "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg=="],
|
"@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="],
|
||||||
|
|
||||||
"@tanstack/react-query": ["@tanstack/react-query@5.90.12", "", { "dependencies": { "@tanstack/query-core": "5.90.12" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg=="],
|
"@tanstack/react-query": ["@tanstack/react-query@5.90.20", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw=="],
|
||||||
|
|
||||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||||
|
|
||||||
@@ -365,37 +359,37 @@
|
|||||||
|
|
||||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
"@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
|
"@types/react": ["@types/react@19.2.11", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-tORuanb01iEzWvMGVGv2ZDhYZVeRMrw453DCSAIn/5yvcSVnMoUMTyf33nQJLahYEnv9xqrTNbgz4qY5EfSh0g=="],
|
||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
|
|
||||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.50.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/type-utils": "8.50.0", "@typescript-eslint/utils": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.50.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg=="],
|
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.54.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/type-utils": "8.54.0", "@typescript-eslint/utils": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.54.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ=="],
|
||||||
|
|
||||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.50.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", "@typescript-eslint/typescript-estree": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q=="],
|
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.54.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA=="],
|
||||||
|
|
||||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.50.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.50.0", "@typescript-eslint/types": "^8.50.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ=="],
|
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.54.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.54.0", "@typescript-eslint/types": "^8.54.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g=="],
|
||||||
|
|
||||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1" } }, "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A=="],
|
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0" } }, "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg=="],
|
||||||
|
|
||||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.50.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w=="],
|
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.54.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw=="],
|
||||||
|
|
||||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "@typescript-eslint/typescript-estree": "8.50.0", "@typescript-eslint/utils": "8.50.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw=="],
|
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/utils": "8.54.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA=="],
|
||||||
|
|
||||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.46.1", "", {}, "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ=="],
|
"@typescript-eslint/types": ["@typescript-eslint/types@8.54.0", "", {}, "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.50.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.50.0", "@typescript-eslint/tsconfig-utils": "8.50.0", "@typescript-eslint/types": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ=="],
|
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.54.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.54.0", "@typescript-eslint/tsconfig-utils": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA=="],
|
||||||
|
|
||||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ=="],
|
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.54.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA=="],
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q=="],
|
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA=="],
|
||||||
|
|
||||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||||
|
|
||||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.53", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ=="],
|
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.3", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.2", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-NVUnA6gQCl8jfoYqKqQU5Clv0aPw14KkZYCsX6T9Lfu9slI0LOU10OTwFHS/WmptsMMpshNd/1tuWsHQ2Uk+cg=="],
|
||||||
|
|
||||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||||
|
|
||||||
@@ -411,7 +405,7 @@
|
|||||||
|
|
||||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||||
|
|
||||||
"axios": ["axios@1.13.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA=="],
|
"axios": ["axios@1.13.4", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg=="],
|
||||||
|
|
||||||
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
||||||
|
|
||||||
@@ -419,8 +413,6 @@
|
|||||||
|
|
||||||
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
||||||
|
|
||||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
|
||||||
|
|
||||||
"browserslist": ["browserslist@4.24.5", "", { "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw=="],
|
"browserslist": ["browserslist@4.24.5", "", { "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw=="],
|
||||||
|
|
||||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||||
@@ -503,7 +495,7 @@
|
|||||||
|
|
||||||
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="],
|
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="],
|
||||||
|
|
||||||
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.26", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ=="],
|
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.5.0", "", { "peerDependencies": { "eslint": ">=9" } }, "sha512-ZYvmh7VfVgqR/7wR71I3Zl6hK/C5CcxdWYKZSpHawS5JCNgE4efhQWg/+/WPpgGAp9Ngp/rRZYyaIwmPQBq/lA=="],
|
||||||
|
|
||||||
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
||||||
|
|
||||||
@@ -525,20 +517,14 @@
|
|||||||
|
|
||||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||||
|
|
||||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
|
||||||
|
|
||||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||||
|
|
||||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||||
|
|
||||||
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
|
||||||
|
|
||||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||||
|
|
||||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
|
||||||
|
|
||||||
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||||
|
|
||||||
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
||||||
@@ -563,7 +549,7 @@
|
|||||||
|
|
||||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||||
|
|
||||||
"globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="],
|
"globals": ["globals@17.3.0", "", {}, "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw=="],
|
||||||
|
|
||||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||||
|
|
||||||
@@ -589,7 +575,7 @@
|
|||||||
|
|
||||||
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
|
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
|
||||||
|
|
||||||
"i18next": ["i18next@25.7.3", "", { "dependencies": { "@babel/runtime": "^7.28.4" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-2XaT+HpYGuc2uTExq9TVRhLsso+Dxym6PWaKpn36wfBmTI779OQ7iP/XaZHzrnGyzU4SHpFrTYLKfVyBfAhVNA=="],
|
"i18next": ["i18next@25.8.3", "", { "dependencies": { "@babel/runtime": "^7.28.4" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-IC/pp2vkczdu1sBheq1eC92bLavN6fM5jH61c7Xa23PGio5ePEd+EP+re1IkO7KEM9eyeJHUxvIRxsaYTlsSyQ=="],
|
||||||
|
|
||||||
"i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.0", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g=="],
|
"i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.0", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g=="],
|
||||||
|
|
||||||
@@ -617,8 +603,6 @@
|
|||||||
|
|
||||||
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
|
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
|
||||||
|
|
||||||
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
|
||||||
|
|
||||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||||
|
|
||||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
@@ -675,7 +659,7 @@
|
|||||||
|
|
||||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
||||||
"lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="],
|
"lucide-react": ["lucide-react@0.563.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA=="],
|
||||||
|
|
||||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|
||||||
@@ -697,8 +681,6 @@
|
|||||||
|
|
||||||
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
|
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
|
||||||
|
|
||||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
|
||||||
|
|
||||||
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
|
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
|
||||||
|
|
||||||
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
||||||
@@ -741,8 +723,6 @@
|
|||||||
|
|
||||||
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
||||||
|
|
||||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
|
||||||
|
|
||||||
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
@@ -781,7 +761,7 @@
|
|||||||
|
|
||||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||||
|
|
||||||
"prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="],
|
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
|
||||||
|
|
||||||
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
||||||
|
|
||||||
@@ -789,15 +769,13 @@
|
|||||||
|
|
||||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
|
||||||
|
|
||||||
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
|
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
|
||||||
|
|
||||||
"react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="],
|
"react-hook-form": ["react-hook-form@7.71.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w=="],
|
||||||
|
|
||||||
"react-hook-form": ["react-hook-form@7.68.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q=="],
|
"react-i18next": ["react-i18next@16.5.4", "", { "dependencies": { "@babel/runtime": "^7.28.4", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-6yj+dcfMncEC21QPhOTsW8mOSO+pzFmT6uvU7XXdvM/Cp38zJkmTeMeKmTrmCMD5ToT79FmiE/mRWiYWcJYW4g=="],
|
||||||
|
|
||||||
"react-i18next": ["react-i18next@16.5.0", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-IMpPTyCTKxEj8klCrLKUTIUa8uYTd851+jcu2fJuUB9Agkk9Qq8asw4omyeHVnOXHrLgQJGTm5zTvn8HpaPiqw=="],
|
|
||||||
|
|
||||||
"react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],
|
"react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],
|
||||||
|
|
||||||
@@ -807,7 +785,7 @@
|
|||||||
|
|
||||||
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
||||||
|
|
||||||
"react-router": ["react-router@7.11.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ=="],
|
"react-router": ["react-router@7.13.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw=="],
|
||||||
|
|
||||||
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
||||||
|
|
||||||
@@ -817,12 +795,8 @@
|
|||||||
|
|
||||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||||
|
|
||||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
|
||||||
|
|
||||||
"rollup": ["rollup@4.46.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.46.2", "@rollup/rollup-android-arm64": "4.46.2", "@rollup/rollup-darwin-arm64": "4.46.2", "@rollup/rollup-darwin-x64": "4.46.2", "@rollup/rollup-freebsd-arm64": "4.46.2", "@rollup/rollup-freebsd-x64": "4.46.2", "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", "@rollup/rollup-linux-arm-musleabihf": "4.46.2", "@rollup/rollup-linux-arm64-gnu": "4.46.2", "@rollup/rollup-linux-arm64-musl": "4.46.2", "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", "@rollup/rollup-linux-ppc64-gnu": "4.46.2", "@rollup/rollup-linux-riscv64-gnu": "4.46.2", "@rollup/rollup-linux-riscv64-musl": "4.46.2", "@rollup/rollup-linux-s390x-gnu": "4.46.2", "@rollup/rollup-linux-x64-gnu": "4.46.2", "@rollup/rollup-linux-x64-musl": "4.46.2", "@rollup/rollup-win32-arm64-msvc": "4.46.2", "@rollup/rollup-win32-ia32-msvc": "4.46.2", "@rollup/rollup-win32-x64-msvc": "4.46.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg=="],
|
"rollup": ["rollup@4.46.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.46.2", "@rollup/rollup-android-arm64": "4.46.2", "@rollup/rollup-darwin-arm64": "4.46.2", "@rollup/rollup-darwin-x64": "4.46.2", "@rollup/rollup-freebsd-arm64": "4.46.2", "@rollup/rollup-freebsd-x64": "4.46.2", "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", "@rollup/rollup-linux-arm-musleabihf": "4.46.2", "@rollup/rollup-linux-arm64-gnu": "4.46.2", "@rollup/rollup-linux-arm64-musl": "4.46.2", "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", "@rollup/rollup-linux-ppc64-gnu": "4.46.2", "@rollup/rollup-linux-riscv64-gnu": "4.46.2", "@rollup/rollup-linux-riscv64-musl": "4.46.2", "@rollup/rollup-linux-s390x-gnu": "4.46.2", "@rollup/rollup-linux-x64-gnu": "4.46.2", "@rollup/rollup-linux-x64-musl": "4.46.2", "@rollup/rollup-win32-arm64-msvc": "4.46.2", "@rollup/rollup-win32-ia32-msvc": "4.46.2", "@rollup/rollup-win32-x64-msvc": "4.46.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg=="],
|
||||||
|
|
||||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
|
||||||
|
|
||||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
@@ -857,13 +831,11 @@
|
|||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|
||||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
|
||||||
|
|
||||||
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||||
|
|
||||||
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||||
|
|
||||||
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
|
"ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
|
||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
@@ -873,7 +845,7 @@
|
|||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
"typescript-eslint": ["typescript-eslint@8.50.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.50.0", "@typescript-eslint/parser": "8.50.0", "@typescript-eslint/typescript-estree": "8.50.0", "@typescript-eslint/utils": "8.50.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A=="],
|
"typescript-eslint": ["typescript-eslint@8.54.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.54.0", "@typescript-eslint/parser": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/utils": "8.54.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
|
||||||
@@ -903,7 +875,7 @@
|
|||||||
|
|
||||||
"vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="],
|
"vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="],
|
||||||
|
|
||||||
"vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="],
|
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
||||||
|
|
||||||
"void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="],
|
"void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="],
|
||||||
|
|
||||||
@@ -915,33 +887,25 @@
|
|||||||
|
|
||||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
"zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="],
|
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||||
|
|
||||||
"zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
|
"zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
|
||||||
|
|
||||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||||
|
|
||||||
"@babel/core/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
"@babel/core/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||||
|
|
||||||
"@babel/generator/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
"@babel/core/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
"@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.27.1", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.1", "@babel/parser": "^7.27.1", "@babel/template": "^7.27.1", "@babel/types": "^7.27.1", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg=="],
|
"@babel/generator/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||||
|
|
||||||
"@babel/helper-module-imports/@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="],
|
|
||||||
|
|
||||||
"@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="],
|
|
||||||
|
|
||||||
"@babel/helpers/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
|
||||||
|
|
||||||
"@babel/parser/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
"@babel/parser/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
||||||
|
|
||||||
"@babel/template/@babel/parser": ["@babel/parser@7.27.2", "", { "dependencies": { "@babel/types": "^7.27.1" }, "bin": "./bin/babel-parser.js" }, "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw=="],
|
"@babel/template/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||||
|
|
||||||
"@babel/template/@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="],
|
"@babel/traverse/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||||
|
|
||||||
"@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
"@babel/traverse/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
"@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
|
||||||
|
|
||||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||||
|
|
||||||
@@ -995,108 +959,114 @@
|
|||||||
|
|
||||||
"@types/estree-jsx/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
"@types/estree-jsx/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0" } }, "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A=="],
|
"@typescript-eslint/eslint-plugin/@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.50.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", "@typescript-eslint/typescript-estree": "8.50.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg=="],
|
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.4", "", {}, "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="],
|
"@typescript-eslint/parser/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
"@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0" } }, "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A=="],
|
"@typescript-eslint/project-service/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
"@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.50.0", "", {}, "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w=="],
|
"@typescript-eslint/type-utils/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
"@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.50.0", "", {}, "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w=="],
|
"@typescript-eslint/typescript-estree/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
"@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.50.0", "", {}, "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.50.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", "@typescript-eslint/typescript-estree": "8.50.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.50.0", "", {}, "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||||
|
|
||||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.46.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.46.1", "@typescript-eslint/tsconfig-utils": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg=="],
|
"@typescript-eslint/utils/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.50.0", "", {}, "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w=="],
|
|
||||||
|
|
||||||
"eslint-plugin-react-hooks/@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="],
|
"eslint-plugin-react-hooks/@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="],
|
||||||
|
|
||||||
"eslint-plugin-react-hooks/zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="],
|
"eslint-plugin-react-hooks/zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="],
|
||||||
|
|
||||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
|
||||||
|
|
||||||
"hast-util-to-jsx-runtime/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
"hast-util-to-jsx-runtime/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
||||||
|
|
||||||
"i18next-browser-languagedetector/@babel/runtime": ["@babel/runtime@7.27.1", "", {}, "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog=="],
|
"i18next-browser-languagedetector/@babel/runtime": ["@babel/runtime@7.27.1", "", {}, "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog=="],
|
||||||
|
|
||||||
"i18next-resources-to-backend/@babel/runtime": ["@babel/runtime@7.27.1", "", {}, "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog=="],
|
"i18next-resources-to-backend/@babel/runtime": ["@babel/runtime@7.27.1", "", {}, "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog=="],
|
||||||
|
|
||||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
|
||||||
|
|
||||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||||
|
|
||||||
"typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.50.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", "@typescript-eslint/typescript-estree": "8.50.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg=="],
|
"@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
|
|
||||||
"@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.27.1", "", { "dependencies": { "@babel/parser": "^7.27.1", "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w=="],
|
|
||||||
|
|
||||||
"@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.27.2", "", { "dependencies": { "@babel/types": "^7.27.1" }, "bin": "./bin/babel-parser.js" }, "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw=="],
|
|
||||||
|
|
||||||
"@babel/helper-module-imports/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
|
||||||
|
|
||||||
"@babel/helper-module-transforms/@babel/traverse/@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
|
|
||||||
|
|
||||||
"@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
|
|
||||||
|
|
||||||
"@babel/helper-module-transforms/@babel/traverse/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
|
|
||||||
|
|
||||||
"@eslint/eslintrc/espree/acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
|
"@eslint/eslintrc/espree/acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
|
||||||
|
|
||||||
"@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
|
"@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.50.0", "", {}, "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w=="],
|
"@types/babel__core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.50.0", "", {}, "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w=="],
|
"@types/babel__generator/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
|
|
||||||
"@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0" } }, "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A=="],
|
"@types/babel__template/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
|
|
||||||
|
"@types/babel__traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||||
|
|
||||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.46.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.46.1", "@typescript-eslint/types": "^8.46.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg=="],
|
"@typescript-eslint/utils/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||||
|
|
||||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.46.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g=="],
|
"eslint-plugin-react-hooks/@babel/core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"eslint-plugin-react-hooks/@babel/core/@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
|
"eslint-plugin-react-hooks/@babel/core/@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
"eslint-plugin-react-hooks/@babel/core/@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="],
|
"eslint-plugin-react-hooks/@babel/core/@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="],
|
||||||
|
|
||||||
"eslint-plugin-react-hooks/@babel/core/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
"eslint-plugin-react-hooks/@babel/core/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
||||||
|
|
||||||
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0" } }, "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A=="],
|
"eslint-plugin-react-hooks/@babel/core/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
|
|
||||||
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.50.0", "", {}, "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w=="],
|
|
||||||
|
|
||||||
"@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
|
||||||
|
|
||||||
"@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
|
||||||
|
|
||||||
"eslint-plugin-react-hooks/@babel/core/@babel/generator/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
|
"eslint-plugin-react-hooks/@babel/core/@babel/generator/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
|
||||||
|
|
||||||
"eslint-plugin-react-hooks/@babel/core/@babel/generator/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
|
"eslint-plugin-react-hooks/@babel/core/@babel/generator/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
|
||||||
|
|
||||||
"@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.27.2", "", {}, "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ=="],
|
||||||
|
|
||||||
"@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/template/@babel/parser": ["@babel/parser@7.27.2", "", { "dependencies": { "@babel/types": "^7.27.1" }, "bin": "./bin/babel-parser.js" }, "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/template/@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/generator/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.27.1", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.1", "@babel/parser": "^7.27.1", "@babel/template": "^7.27.1", "@babel/types": "^7.27.1", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/traverse/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/template/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.27.1", "", { "dependencies": { "@babel/parser": "^7.27.1", "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.27.2", "", { "dependencies": { "@babel/types": "^7.27.1" }, "bin": "./bin/babel-parser.js" }, "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,42 +18,42 @@
|
|||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tanstack/react-query": "^5.90.12",
|
"@tanstack/react-query": "^5.90.20",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"i18next": "^25.7.3",
|
"i18next": "^25.8.3",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
"i18next-resources-to-backend": "^1.2.1",
|
"i18next-resources-to-backend": "^1.2.1",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.563.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.4",
|
||||||
"react-hook-form": "^7.68.0",
|
"react-hook-form": "^7.71.1",
|
||||||
"react-i18next": "^16.5.0",
|
"react-i18next": "^16.5.4",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-router": "^7.11.0",
|
"react-router": "^7.13.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"zod": "^4.2.1"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@tanstack/eslint-plugin-query": "^5.91.2",
|
"@tanstack/eslint-plugin-query": "^5.91.4",
|
||||||
"@types/node": "^25.0.3",
|
"@types/node": "^25.2.0",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.11",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.2",
|
"@vitejs/plugin-react": "^5.1.3",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.26",
|
"eslint-plugin-react-refresh": "^0.5.0",
|
||||||
"globals": "^16.5.0",
|
"globals": "^17.3.0",
|
||||||
"prettier": "3.7.4",
|
"prettier": "3.8.1",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.50.0",
|
"typescript-eslint": "^8.54.0",
|
||||||
"vite": "^7.3.0"
|
"vite": "^7.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,10 @@ import z from "zod";
|
|||||||
interface Props {
|
interface Props {
|
||||||
formId: string;
|
formId: string;
|
||||||
onSubmit: (code: TotpSchema) => void;
|
onSubmit: (code: TotpSchema) => void;
|
||||||
loading?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TotpForm = (props: Props) => {
|
export const TotpForm = (props: Props) => {
|
||||||
const { formId, onSubmit, loading } = props;
|
const { formId, onSubmit } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
z.config({
|
z.config({
|
||||||
@@ -30,6 +29,14 @@ export const TotpForm = (props: Props) => {
|
|||||||
resolver: zodResolver(totpSchema),
|
resolver: zodResolver(totpSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleChange = (value: string) => {
|
||||||
|
form.setValue("code", value, { shouldDirty: true, shouldValidate: true });
|
||||||
|
|
||||||
|
if (value.length === 6) {
|
||||||
|
onSubmit({ code: value });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form id={formId} onSubmit={form.handleSubmit(onSubmit)}>
|
<form id={formId} onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
@@ -41,10 +48,10 @@ export const TotpForm = (props: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<InputOTP
|
<InputOTP
|
||||||
maxLength={6}
|
maxLength={6}
|
||||||
disabled={loading}
|
|
||||||
{...field}
|
{...field}
|
||||||
autoComplete="one-time-code"
|
autoComplete="one-time-code"
|
||||||
autoFocus
|
autoFocus
|
||||||
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
<InputOTPGroup>
|
<InputOTPGroup>
|
||||||
<InputOTPSlot index={0} />
|
<InputOTPSlot index={0} />
|
||||||
|
|||||||
@@ -159,6 +159,10 @@ code {
|
|||||||
@apply relative rounded bg-muted px-[0.2rem] py-[0.1rem] font-mono text-sm font-semibold break-all;
|
@apply relative rounded bg-muted px-[0.2rem] py-[0.1rem] font-mono text-sm font-semibold break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
@apply bg-accent border border-border rounded-md p-2 whitespace-break-spaces;
|
||||||
|
}
|
||||||
|
|
||||||
.lead {
|
.lead {
|
||||||
@apply text-xl text-muted-foreground;
|
@apply text-xl text-muted-foreground;
|
||||||
}
|
}
|
||||||
|
|||||||
53
frontend/src/lib/hooks/oidc.ts
Normal file
53
frontend/src/lib/hooks/oidc.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
export type OIDCValues = {
|
||||||
|
scope: string;
|
||||||
|
response_type: string;
|
||||||
|
client_id: string;
|
||||||
|
redirect_uri: string;
|
||||||
|
state: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IuseOIDCParams {
|
||||||
|
values: OIDCValues;
|
||||||
|
compiled: string;
|
||||||
|
isOidc: boolean;
|
||||||
|
missingParams: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const optionalParams: string[] = ["state"];
|
||||||
|
|
||||||
|
export function useOIDCParams(params: URLSearchParams): IuseOIDCParams {
|
||||||
|
let compiled: string = "";
|
||||||
|
let isOidc = false;
|
||||||
|
const missingParams: string[] = [];
|
||||||
|
|
||||||
|
const values: OIDCValues = {
|
||||||
|
scope: params.get("scope") ?? "",
|
||||||
|
response_type: params.get("response_type") ?? "",
|
||||||
|
client_id: params.get("client_id") ?? "",
|
||||||
|
redirect_uri: params.get("redirect_uri") ?? "",
|
||||||
|
state: params.get("state") ?? "",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const key of Object.keys(values)) {
|
||||||
|
if (!values[key as keyof OIDCValues]) {
|
||||||
|
if (!optionalParams.includes(key)) {
|
||||||
|
missingParams.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingParams.length === 0) {
|
||||||
|
isOidc = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOidc) {
|
||||||
|
compiled = new URLSearchParams(values).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
values,
|
||||||
|
compiled,
|
||||||
|
isOidc,
|
||||||
|
missingParams,
|
||||||
|
};
|
||||||
|
}
|
||||||
64
frontend/src/lib/hooks/redirect-uri.ts
Normal file
64
frontend/src/lib/hooks/redirect-uri.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
type IuseRedirectUri = {
|
||||||
|
url?: URL;
|
||||||
|
valid: boolean;
|
||||||
|
trusted: boolean;
|
||||||
|
allowedProto: boolean;
|
||||||
|
httpsDowngrade: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRedirectUri = (
|
||||||
|
redirect_uri: string | null,
|
||||||
|
cookieDomain: string,
|
||||||
|
): IuseRedirectUri => {
|
||||||
|
let isValid = false;
|
||||||
|
let isTrusted = false;
|
||||||
|
let isAllowedProto = false;
|
||||||
|
let isHttpsDowngrade = false;
|
||||||
|
|
||||||
|
if (!redirect_uri) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
trusted: false,
|
||||||
|
allowedProto: false,
|
||||||
|
httpsDowngrade: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let url: URL;
|
||||||
|
|
||||||
|
try {
|
||||||
|
url = new URL(redirect_uri);
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
trusted: false,
|
||||||
|
allowedProto: false,
|
||||||
|
httpsDowngrade: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid = true;
|
||||||
|
|
||||||
|
if (
|
||||||
|
url.hostname == cookieDomain ||
|
||||||
|
url.hostname.endsWith(`.${cookieDomain}`)
|
||||||
|
) {
|
||||||
|
isTrusted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.protocol == "http:" || url.protocol == "https:") {
|
||||||
|
isAllowedProto = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.location.protocol == "https:" && url.protocol == "http:") {
|
||||||
|
isHttpsDowngrade = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
valid: isValid,
|
||||||
|
trusted: isTrusted,
|
||||||
|
allowedProto: isAllowedProto,
|
||||||
|
httpsDowngrade: isHttpsDowngrade,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Forgot your password?",
|
||||||
"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:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "نسيت كلمة المرور؟",
|
"forgotPasswordTitle": "نسيت كلمة المرور؟",
|
||||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||||
"errorTitle": "حدث خطأ",
|
"errorTitle": "حدث خطأ",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "تجاهل",
|
"ignoreTitle": "تجاهل",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Forgot your password?",
|
||||||
"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:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"loginOauthFailSubtitle": "Nepodařilo se získat OAuth URL",
|
"loginOauthFailSubtitle": "Nepodařilo se získat OAuth URL",
|
||||||
"loginOauthSuccessTitle": "Přesměrování",
|
"loginOauthSuccessTitle": "Přesměrování",
|
||||||
"loginOauthSuccessSubtitle": "Přesměrování k poskytovateli OAuth",
|
"loginOauthSuccessSubtitle": "Přesměrování k poskytovateli OAuth",
|
||||||
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
|
"loginOauthAutoRedirectTitle": "Automatické přesměrování OAuth",
|
||||||
"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": "Redirect now",
|
||||||
"continueTitle": "Pokračovat",
|
"continueTitle": "Pokračovat",
|
||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Zapomněli jste heslo?",
|
"forgotPasswordTitle": "Zapomněli jste heslo?",
|
||||||
"failedToFetchProvidersTitle": "Nepodařilo se načíst poskytovatele ověřování. Zkontrolujte prosím konfiguraci.",
|
"failedToFetchProvidersTitle": "Nepodařilo se načíst poskytovatele ověřování. Zkontrolujte prosím konfiguraci.",
|
||||||
"errorTitle": "Došlo k chybě",
|
"errorTitle": "Došlo k chybě",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "Nastala chyba při pokusu o provedení této akce. Pro více informací prosím zkontrolujte konzolu.",
|
"errorSubtitle": "Nastala chyba při pokusu o provedení této akce. Pro více informací prosím zkontrolujte konzolu.",
|
||||||
"forgotPasswordMessage": "Heslo můžete obnovit změnou proměnné `USERS`.",
|
"forgotPasswordMessage": "Heslo můžete obnovit změnou proměnné `USERS`.",
|
||||||
"fieldRequired": "Toto pole je povinné",
|
"fieldRequired": "Toto pole je povinné",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Glemt din adgangskode?",
|
"forgotPasswordTitle": "Glemt din adgangskode?",
|
||||||
"failedToFetchProvidersTitle": "Kunne ikke indlæse godkendelsesudbydere. Tjek venligst din konfiguration.",
|
"failedToFetchProvidersTitle": "Kunne ikke indlæse godkendelsesudbydere. Tjek venligst din konfiguration.",
|
||||||
"errorTitle": "Der opstod en fejl",
|
"errorTitle": "Der opstod en fejl",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "Der opstod en fejl under forsøget på at udføre denne handling. Tjek venligst konsollen for mere information.",
|
"errorSubtitle": "Der opstod en fejl under forsøget på at udføre denne handling. Tjek venligst konsollen for mere information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,17 +14,17 @@
|
|||||||
"loginOauthFailSubtitle": "Fehler beim Abrufen der OAuth-URL",
|
"loginOauthFailSubtitle": "Fehler beim Abrufen der OAuth-URL",
|
||||||
"loginOauthSuccessTitle": "Leite weiter",
|
"loginOauthSuccessTitle": "Leite weiter",
|
||||||
"loginOauthSuccessSubtitle": "Weiterleitung zu Ihrem OAuth-Provider",
|
"loginOauthSuccessSubtitle": "Weiterleitung zu Ihrem OAuth-Provider",
|
||||||
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
|
"loginOauthAutoRedirectTitle": "Automatische OAuth-Weiterleitung",
|
||||||
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
|
"loginOauthAutoRedirectSubtitle": "Sie werden automatisch zu Ihrem OAuth-Anbieter weitergeleitet, um sich zu authentifizieren.",
|
||||||
"loginOauthAutoRedirectButton": "Redirect now",
|
"loginOauthAutoRedirectButton": "Jetzt weiterleiten",
|
||||||
"continueTitle": "Weiter",
|
"continueTitle": "Weiter",
|
||||||
"continueRedirectingTitle": "Leite weiter...",
|
"continueRedirectingTitle": "Leite weiter...",
|
||||||
"continueRedirectingSubtitle": "Sie sollten in Kürze zur App weitergeleitet werden",
|
"continueRedirectingSubtitle": "Sie sollten in Kürze zur App weitergeleitet werden",
|
||||||
"continueRedirectManually": "Redirect me manually",
|
"continueRedirectManually": "Manuell weiterleiten",
|
||||||
"continueInsecureRedirectTitle": "Unsichere Weiterleitung",
|
"continueInsecureRedirectTitle": "Unsichere Weiterleitung",
|
||||||
"continueInsecureRedirectSubtitle": "Sie versuchen von <code>https</code> auf <code>http</code> weiterzuleiten, was unsicher ist. Sind Sie sicher, dass Sie fortfahren möchten?",
|
"continueInsecureRedirectSubtitle": "Sie versuchen von <code>https</code> auf <code>http</code> weiterzuleiten, was unsicher ist. Sind Sie sicher, dass Sie fortfahren möchten?",
|
||||||
"continueUntrustedRedirectTitle": "Untrusted redirect",
|
"continueUntrustedRedirectTitle": "Nicht vertrauenswürdige Weiterleitung",
|
||||||
"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": "Sie versuchen auf eine Domain umzuleiten, die nicht mit Ihrer konfigurierten Domain übereinstimmt (<code>{{cookieDomain}}</code>). Sind Sie sicher, dass Sie fortfahren möchten?",
|
||||||
"logoutFailTitle": "Abmelden fehlgeschlagen",
|
"logoutFailTitle": "Abmelden fehlgeschlagen",
|
||||||
"logoutFailSubtitle": "Bitte versuchen Sie es erneut",
|
"logoutFailSubtitle": "Bitte versuchen Sie es erneut",
|
||||||
"logoutSuccessTitle": "Abgemeldet",
|
"logoutSuccessTitle": "Abgemeldet",
|
||||||
@@ -51,12 +51,31 @@
|
|||||||
"forgotPasswordTitle": "Passwort vergessen?",
|
"forgotPasswordTitle": "Passwort vergessen?",
|
||||||
"failedToFetchProvidersTitle": "Fehler beim Laden der Authentifizierungsanbieter. Bitte überprüfen Sie Ihre Konfiguration.",
|
"failedToFetchProvidersTitle": "Fehler beim Laden der Authentifizierungsanbieter. Bitte überprüfen Sie Ihre Konfiguration.",
|
||||||
"errorTitle": "Ein Fehler ist aufgetreten",
|
"errorTitle": "Ein Fehler ist aufgetreten",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "Beim Versuch, diese Aktion auszuführen, ist ein Fehler aufgetreten. Bitte überprüfen Sie die Konsole für weitere Informationen.",
|
"errorSubtitle": "Beim Versuch, diese Aktion auszuführen, ist ein Fehler aufgetreten. Bitte überprüfen Sie die Konsole für weitere Informationen.",
|
||||||
"forgotPasswordMessage": "Das Passwort kann durch Änderung der 'USERS' Variable zurückgesetzt werden.",
|
"forgotPasswordMessage": "Das Passwort kann durch Änderung der 'USERS' Variable zurückgesetzt werden.",
|
||||||
"fieldRequired": "Dieses Feld ist notwendig",
|
"fieldRequired": "Dieses Feld ist notwendig",
|
||||||
"invalidInput": "Ungültige Eingabe",
|
"invalidInput": "Ungültige Eingabe",
|
||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Ungültige Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "Diese Instanz ist so konfiguriert, dass sie von <code>{{appUrl}}</code> aufgerufen werden kann, aber <code>{{currentUrl}}</code> wird verwendet. Wenn Sie fortfahren, können Probleme bei der Authentifizierung auftreten.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignorieren",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Zur korrekten Domain gehen",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Ξεχάσατε το συνθηματικό σας;",
|
"forgotPasswordTitle": "Ξεχάσατε το συνθηματικό σας;",
|
||||||
"failedToFetchProvidersTitle": "Αποτυχία φόρτωσης παρόχων πιστοποίησης. Παρακαλώ ελέγξτε τις ρυθμίσεις σας.",
|
"failedToFetchProvidersTitle": "Αποτυχία φόρτωσης παρόχων πιστοποίησης. Παρακαλώ ελέγξτε τις ρυθμίσεις σας.",
|
||||||
"errorTitle": "Παρουσιάστηκε ένα σφάλμα",
|
"errorTitle": "Παρουσιάστηκε ένα σφάλμα",
|
||||||
|
"errorSubtitleInfo": "Το ακόλουθο σφάλμα προέκυψε κατά την επεξεργασία του αιτήματός σας:",
|
||||||
"errorSubtitle": "Παρουσιάστηκε σφάλμα κατά την προσπάθεια εκτέλεσης αυτής της ενέργειας. Ελέγξτε την κονσόλα για περισσότερες πληροφορίες.",
|
"errorSubtitle": "Παρουσιάστηκε σφάλμα κατά την προσπάθεια εκτέλεσης αυτής της ενέργειας. Ελέγξτε την κονσόλα για περισσότερες πληροφορίες.",
|
||||||
"forgotPasswordMessage": "Μπορείτε να επαναφέρετε τον κωδικό πρόσβασής σας αλλάζοντας τη μεταβλητή περιβάλλοντος `USERS`.",
|
"forgotPasswordMessage": "Μπορείτε να επαναφέρετε τον κωδικό πρόσβασής σας αλλάζοντας τη μεταβλητή περιβάλλοντος `USERS`.",
|
||||||
"fieldRequired": "Αυτό το πεδίο είναι υποχρεωτικό",
|
"fieldRequired": "Αυτό το πεδίο είναι υποχρεωτικό",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Μη έγκυρο domain",
|
"domainWarningTitle": "Μη έγκυρο domain",
|
||||||
"domainWarningSubtitle": "Αυτή η εφαρμογή έχει ρυθμιστεί για πρόσβαση από <code>{{appUrl}}</code>, αλλά <code>{{currentUrl}}</code> χρησιμοποιείται. Αν συνεχίσετε, μπορεί να αντιμετωπίσετε προβλήματα με την ταυτοποίηση.",
|
"domainWarningSubtitle": "Αυτή η εφαρμογή έχει ρυθμιστεί για πρόσβαση από <code>{{appUrl}}</code>, αλλά <code>{{currentUrl}}</code> χρησιμοποιείται. Αν συνεχίσετε, μπορεί να αντιμετωπίσετε προβλήματα με την ταυτοποίηση.",
|
||||||
"ignoreTitle": "Παράβλεψη",
|
"ignoreTitle": "Παράβλεψη",
|
||||||
"goToCorrectDomainTitle": "Μεταβείτε στο σωστό domain"
|
"goToCorrectDomainTitle": "Μεταβείτε στο σωστό domain",
|
||||||
}
|
"authorizeTitle": "Εξουσιοδότηση",
|
||||||
|
"authorizeCardTitle": "Συνέχεια στην εφαρμογή {{app}};",
|
||||||
|
"authorizeSubtitle": "Θα θέλατε να συνεχίσετε σε αυτή την εφαρμογή; Παρακαλώ ελέγξτε προσεκτικά τα δικαιώματα που ζητούνται από την εφαρμογή.",
|
||||||
|
"authorizeSubtitleOAuth": "Θα θέλατε να συνεχίσετε σε αυτή την εφαρμογή;",
|
||||||
|
"authorizeLoadingTitle": "Φόρτωση...",
|
||||||
|
"authorizeLoadingSubtitle": "Παρακαλώ περιμένετε όσο φορτώνουμε τις απαραίτητες πληροφορίες.",
|
||||||
|
"authorizeSuccessTitle": "Εξουσιοδοτημένος",
|
||||||
|
"authorizeSuccessSubtitle": "Θα μεταφερθείτε στην εφαρμογή σε λίγα δευτερόλεπτα.",
|
||||||
|
"authorizeErrorClientInfo": "Παρουσιάστηκε σφάλμα κατά τη φόρτωση των πληροφοριών. Παρακαλώ προσπαθήστε ξανά αργότερα.",
|
||||||
|
"authorizeErrorMissingParams": "Οι παρακάτω απαραίτητες πληροφορίες λείπουν από το αίτημά σας: {{missingParams}}",
|
||||||
|
"openidScopeName": "Σύνδεση OpenID",
|
||||||
|
"openidScopeDescription": "Επιτρέπει στην εφαρμογή την πρόσβαση στις πληροφορίες σύνδεσης OpenID.",
|
||||||
|
"emailScopeName": "Διεύθυνση ηλεκτρονικού ταχυδρομείου",
|
||||||
|
"emailScopeDescription": "Επιτρέπει στην εφαρμογή να έχει πρόσβαση στη διεύθυνση ηλεκτρονικού ταχυδρομείου σας.",
|
||||||
|
"profileScopeName": "Προφίλ",
|
||||||
|
"profileScopeDescription": "Επιτρέπει στην εφαρμογή να έχει πρόσβαση στις πληροφορίες του προφίλ σας.",
|
||||||
|
"groupsScopeName": "Ομάδες",
|
||||||
|
"groupsScopeDescription": "Επιτρέπει στην εφαρμογή την πρόσβαση στις πληροφορίες ομάδας σας."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,12 +51,31 @@
|
|||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Forgot your password?",
|
||||||
"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",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check your browser console or the app logs for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
"invalidInput": "Invalid input",
|
"invalidInput": "Invalid input",
|
||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,12 +51,31 @@
|
|||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Forgot your password?",
|
||||||
"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",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check your browser console or the app logs for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
"invalidInput": "Invalid input",
|
"invalidInput": "Invalid input",
|
||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "¿Olvidó su contraseña?",
|
"forgotPasswordTitle": "¿Olvidó su contraseña?",
|
||||||
"failedToFetchProvidersTitle": "Error al cargar los proveedores de autenticación. Por favor revise su configuración.",
|
"failedToFetchProvidersTitle": "Error al cargar los proveedores de autenticación. Por favor revise su configuración.",
|
||||||
"errorTitle": "Ha ocurrido un error",
|
"errorTitle": "Ha ocurrido un error",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "Ocurrió un error mientras se trataba de realizar esta acción. Por favor, revise la consola para más información.",
|
"errorSubtitle": "Ocurrió un error mientras se trataba de realizar esta acción. Por favor, revise la consola para más información.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Unohditko salasanasi?",
|
"forgotPasswordTitle": "Unohditko salasanasi?",
|
||||||
"failedToFetchProvidersTitle": "Todennuspalvelujen tarjoajien lataaminen epäonnistui. Tarkista määrityksesi.",
|
"failedToFetchProvidersTitle": "Todennuspalvelujen tarjoajien lataaminen epäonnistui. Tarkista määrityksesi.",
|
||||||
"errorTitle": "Tapahtui virhe",
|
"errorTitle": "Tapahtui virhe",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "Tapahtui virhe yritettäessä suorittaa tämä toiminto. Ole hyvä ja tarkista konsoli saadaksesi lisätietoja.",
|
"errorSubtitle": "Tapahtui virhe yritettäessä suorittaa tämä toiminto. Ole hyvä ja tarkista konsoli saadaksesi lisätietoja.",
|
||||||
"forgotPasswordMessage": "Voit nollata salasanasi vaihtamalla ympäristömuuttujan `USERS`.",
|
"forgotPasswordMessage": "Voit nollata salasanasi vaihtamalla ympäristömuuttujan `USERS`.",
|
||||||
"fieldRequired": "Tämä kenttä on pakollinen",
|
"fieldRequired": "Tämä kenttä on pakollinen",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Virheellinen verkkotunnus",
|
"domainWarningTitle": "Virheellinen verkkotunnus",
|
||||||
"domainWarningSubtitle": "Tämä instanssi on määritelty käyttämään osoitetta <code>{{appUrl}}</code>, mutta nykyinen osoite on <code>{{currentUrl}}</code>. Jos jatkat, saatat törmätä ongelmiin autentikoinnissa.",
|
"domainWarningSubtitle": "Tämä instanssi on määritelty käyttämään osoitetta <code>{{appUrl}}</code>, mutta nykyinen osoite on <code>{{currentUrl}}</code>. Jos jatkat, saatat törmätä ongelmiin autentikoinnissa.",
|
||||||
"ignoreTitle": "Jätä huomiotta",
|
"ignoreTitle": "Jätä huomiotta",
|
||||||
"goToCorrectDomainTitle": "Siirry oikeaan verkkotunnukseen"
|
"goToCorrectDomainTitle": "Siirry oikeaan verkkotunnukseen",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Mot de passe oublié ?",
|
"forgotPasswordTitle": "Mot de passe oublié ?",
|
||||||
"failedToFetchProvidersTitle": "Échec du chargement des fournisseurs d'authentification. Veuillez vérifier votre configuration.",
|
"failedToFetchProvidersTitle": "Échec du chargement des fournisseurs d'authentification. Veuillez vérifier votre configuration.",
|
||||||
"errorTitle": "Une erreur est survenue",
|
"errorTitle": "Une erreur est survenue",
|
||||||
|
"errorSubtitleInfo": "L'erreur suivante s'est produite lors du traitement de votre requête :",
|
||||||
"errorSubtitle": "Une erreur est survenue lors de l'exécution de cette action. Veuillez consulter la console pour plus d'informations.",
|
"errorSubtitle": "Une erreur est survenue lors de l'exécution de cette action. Veuillez consulter la console pour plus d'informations.",
|
||||||
"forgotPasswordMessage": "Vous pouvez réinitialiser votre mot de passe en modifiant la variable d'environnement `USERS`.",
|
"forgotPasswordMessage": "Vous pouvez réinitialiser votre mot de passe en modifiant la variable d'environnement `USERS`.",
|
||||||
"fieldRequired": "Ce champ est obligatoire",
|
"fieldRequired": "Ce champ est obligatoire",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Domaine invalide",
|
"domainWarningTitle": "Domaine invalide",
|
||||||
"domainWarningSubtitle": "Cette instance est configurée pour être accédée depuis <code>{{appUrl}}</code>, mais <code>{{currentUrl}}</code> est utilisé. Si vous continuez, vous pourriez rencontrer des problèmes d'authentification.",
|
"domainWarningSubtitle": "Cette instance est configurée pour être accédée depuis <code>{{appUrl}}</code>, mais <code>{{currentUrl}}</code> est utilisé. Si vous continuez, vous pourriez rencontrer des problèmes d'authentification.",
|
||||||
"ignoreTitle": "Ignorer",
|
"ignoreTitle": "Ignorer",
|
||||||
"goToCorrectDomainTitle": "Aller au bon domaine"
|
"goToCorrectDomainTitle": "Aller au bon domaine",
|
||||||
}
|
"authorizeTitle": "Autoriser",
|
||||||
|
"authorizeCardTitle": "Continuer vers {{app}} ?",
|
||||||
|
"authorizeSubtitle": "Voulez-vous continuer vers cette application ? Veuillez examiner attentivement les autorisations demandées par l'application.",
|
||||||
|
"authorizeSubtitleOAuth": "Voulez-vous continuer vers cette application ?",
|
||||||
|
"authorizeLoadingTitle": "Chargement...",
|
||||||
|
"authorizeLoadingSubtitle": "Veuillez patienter pendant que nous chargeons les informations du client.",
|
||||||
|
"authorizeSuccessTitle": "Autorisé",
|
||||||
|
"authorizeSuccessSubtitle": "Vous allez être redirigé vers l'application dans quelques secondes.",
|
||||||
|
"authorizeErrorClientInfo": "Une erreur est survenue lors du chargement des informations du client. Veuillez réessayer plus tard.",
|
||||||
|
"authorizeErrorMissingParams": "Les paramètres suivants sont manquants : {{missingParams}}",
|
||||||
|
"openidScopeName": "Connexion OpenID",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Autorise l'application à accéder à votre adresse e-mail.",
|
||||||
|
"profileScopeName": "Profil",
|
||||||
|
"profileScopeDescription": "Autorise l'application à accéder aux informations de votre profil.",
|
||||||
|
"groupsScopeName": "Groupes",
|
||||||
|
"groupsScopeDescription": "Autorise une application à accéder aux informations de votre groupe."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Forgot your password?",
|
||||||
"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:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,42 +1,42 @@
|
|||||||
{
|
{
|
||||||
"loginTitle": "Welcome back, login with",
|
"loginTitle": "Welcome back, login with",
|
||||||
"loginTitleSimple": "Welcome back, please login",
|
"loginTitleSimple": "Üdvözöljük, kérem jelentkezzen be",
|
||||||
"loginDivider": "Or",
|
"loginDivider": "Vagy",
|
||||||
"loginUsername": "Username",
|
"loginUsername": "Felhasználónév",
|
||||||
"loginPassword": "Password",
|
"loginPassword": "Jelszó",
|
||||||
"loginSubmit": "Login",
|
"loginSubmit": "Bejelentkezés",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Sikertelen bejelentkezés",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Kérjük, ellenőrizze a felhasználónevét és jelszavát",
|
||||||
"loginFailRateLimit": "You failed to login too many times. Please try again later",
|
"loginFailRateLimit": "Túl sokszor próbálkoztál bejelentkezni. Próbáld újra később",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Bejelentkezve",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Üdvözöljük!",
|
||||||
"loginOauthFailTitle": "An error occurred",
|
"loginOauthFailTitle": "An error occurred",
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
"loginOauthSuccessTitle": "Átirányítás",
|
||||||
"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": "Redirect now",
|
||||||
"continueTitle": "Continue",
|
"continueTitle": "Continue",
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
"continueRedirectingTitle": "Átirányítás...",
|
||||||
"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": "Insecure redirect",
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
|
"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": "Sikertelen kijelentkezés",
|
||||||
"logoutFailSubtitle": "Please try again",
|
"logoutFailSubtitle": "Próbálja újra",
|
||||||
"logoutSuccessTitle": "Logged out",
|
"logoutSuccessTitle": "Kijelentkezve",
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
"logoutSuccessSubtitle": "Kijelentkeztél",
|
||||||
"logoutTitle": "Logout",
|
"logoutTitle": "Kijelentkezés",
|
||||||
"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": "Page not found",
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
"notFoundSubtitle": "The page you are looking for does not exist.",
|
||||||
"notFoundButton": "Go home",
|
"notFoundButton": "Ugrás a kezdőlapra",
|
||||||
"totpFailTitle": "Failed to verify code",
|
"totpFailTitle": "Érvénytelen kód",
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
"totpFailSubtitle": "Kérjük ellenőrizze a kódot és próbálja újra",
|
||||||
"totpSuccessTitle": "Verified",
|
"totpSuccessTitle": "Verified",
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
@@ -46,17 +46,36 @@
|
|||||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
"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": "Próbálja újra",
|
||||||
"cancelTitle": "Cancel",
|
"cancelTitle": "Mégse",
|
||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Elfelejtette jelszavát?",
|
||||||
"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": "Hiba történt",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "Ez egy kötelező mező",
|
||||||
"invalidInput": "Invalid input",
|
"invalidInput": "Invalid input",
|
||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
{
|
{
|
||||||
"loginTitle": "Welcome back, login with",
|
"loginTitle": "Bentornato, accedi con",
|
||||||
"loginTitleSimple": "Welcome back, please login",
|
"loginTitleSimple": "Bentornato, accedi al tuo account",
|
||||||
"loginDivider": "Or",
|
"loginDivider": "Oppure",
|
||||||
"loginUsername": "Username",
|
"loginUsername": "Nome utente",
|
||||||
"loginPassword": "Password",
|
"loginPassword": "Password",
|
||||||
"loginSubmit": "Login",
|
"loginSubmit": "Accesso",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Accesso non riuscito",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Verifica che il nome utente e la password siano corretti",
|
||||||
"loginFailRateLimit": "You failed to login too many times. Please try again later",
|
"loginFailRateLimit": "Hai effettuato troppi tentativi errati. Riprova più tardi",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Accesso effettuato",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Bentornato!",
|
||||||
"loginOauthFailTitle": "An error occurred",
|
"loginOauthFailTitle": "Si è verificato un errore",
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
"loginOauthFailSubtitle": "Impossibile ottenere l'URL di OAuth",
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
"loginOauthSuccessTitle": "Redirecting",
|
||||||
"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": "Redirect now",
|
||||||
"continueTitle": "Continue",
|
"continueTitle": "Prosegui",
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
"continueRedirectingTitle": "Redirecting...",
|
||||||
"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",
|
||||||
@@ -34,29 +34,48 @@
|
|||||||
"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": "Page not found",
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
"notFoundSubtitle": "The page you are looking for does not exist.",
|
||||||
"notFoundButton": "Go home",
|
"notFoundButton": "Vai alla home",
|
||||||
"totpFailTitle": "Failed to verify code",
|
"totpFailTitle": "Errore nella verifica del codice",
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
"totpFailSubtitle": "Si prega di controllare il codice e riprovare",
|
||||||
"totpSuccessTitle": "Verified",
|
"totpSuccessTitle": "Verificato",
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Reindirizzamento alla tua app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Inserisci il tuo codice TOTP",
|
||||||
"totpSubtitle": "Please enter the code from your authenticator app.",
|
"totpSubtitle": "Inserisci il codice dalla tua app di autenticazione.",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Non Autorizzato",
|
||||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
"unauthorizedResourceSubtitle": "L'utente con username <code>{{username}}</code> non è autorizzato ad accedere alla risorsa <code>{{resource}}</code>.",
|
||||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
"unauthorizedLoginSubtitle": "L'utente con username <code>{{username}}</code> non è autorizzato a effettuare l'accesso.",
|
||||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
"unauthorizedGroupsSubtitle": "L'utente con nome utente <code>{{username}}</code> non fa parte dei gruppi richiesti dalla risorsa <code>{{resource}}</code>.",
|
||||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
"unauthorizedIpSubtitle": "Il tuo indirizzo IP <code>{{ip}}</code> non è autorizzato ad accedere alla risorsa <code>{{resource}}</code>.",
|
||||||
"unauthorizedButton": "Try again",
|
"unauthorizedButton": "Riprova",
|
||||||
"cancelTitle": "Cancel",
|
"cancelTitle": "Annulla",
|
||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Password dimenticata?",
|
||||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
"failedToFetchProvidersTitle": "Impossibile caricare i provider di autenticazione. Si prega di controllare la configurazione.",
|
||||||
"errorTitle": "An error occurred",
|
"errorTitle": "Si è verificato un errore",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "Puoi reimpostare la tua password modificando la variabile d'ambiente `USERS`.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "Questo campo è obbligatorio",
|
||||||
"invalidInput": "Invalid input",
|
"invalidInput": "Input non valido",
|
||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Dominio non valido",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "Questa istanza è configurata per essere accessibile da <code>{{appUrl}}</code>, ma <code>{{currentUrl}}</code> è in uso. Se procedi, potresti incorrere in problemi di autenticazione.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignora",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Vai al dominio corretto",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Forgot your password?",
|
||||||
"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:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Forgot your password?",
|
||||||
"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:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
{
|
{
|
||||||
"loginTitle": "Welkom terug, log in met",
|
"loginTitle": "Welkom terug, log in met",
|
||||||
"loginTitleSimple": "Welcome back, please login",
|
"loginTitleSimple": "Welkom terug, log in",
|
||||||
"loginDivider": "Or",
|
"loginDivider": "Of",
|
||||||
"loginUsername": "Gebruikersnaam",
|
"loginUsername": "Gebruikersnaam",
|
||||||
"loginPassword": "Wachtwoord",
|
"loginPassword": "Wachtwoord",
|
||||||
"loginSubmit": "Log in",
|
"loginSubmit": "Log in",
|
||||||
"loginFailTitle": "Mislukt om in te loggen",
|
"loginFailTitle": "Mislukt om in te loggen",
|
||||||
"loginFailSubtitle": "Controleer je gebruikersnaam en wachtwoord",
|
"loginFailSubtitle": "Controleer je gebruikersnaam en wachtwoord",
|
||||||
"loginFailRateLimit": "You failed to login too many times. Please try again later",
|
"loginFailRateLimit": "Inloggen is te vaak mislukt. Probeer het later opnieuw",
|
||||||
"loginSuccessTitle": "Ingelogd",
|
"loginSuccessTitle": "Ingelogd",
|
||||||
"loginSuccessSubtitle": "Welkom terug!",
|
"loginSuccessSubtitle": "Welkom terug!",
|
||||||
"loginOauthFailTitle": "An error occurred",
|
"loginOauthFailTitle": "Er is een fout opgetreden",
|
||||||
"loginOauthFailSubtitle": "Fout bij het ophalen van OAuth URL",
|
"loginOauthFailSubtitle": "Fout bij het ophalen van OAuth URL",
|
||||||
"loginOauthSuccessTitle": "Omleiden",
|
"loginOauthSuccessTitle": "Omleiden",
|
||||||
"loginOauthSuccessSubtitle": "Omleiden naar je OAuth provider",
|
"loginOauthSuccessSubtitle": "Omleiden naar je OAuth provider",
|
||||||
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
|
"loginOauthAutoRedirectTitle": "OAuth automatische omleiding",
|
||||||
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
|
"loginOauthAutoRedirectSubtitle": "Je wordt automatisch omgeleid naar je OAuth provider om te authenticeren.",
|
||||||
"loginOauthAutoRedirectButton": "Redirect now",
|
"loginOauthAutoRedirectButton": "Nu omleiden",
|
||||||
"continueTitle": "Ga verder",
|
"continueTitle": "Ga verder",
|
||||||
"continueRedirectingTitle": "Omleiden...",
|
"continueRedirectingTitle": "Omleiden...",
|
||||||
"continueRedirectingSubtitle": "Je wordt naar de app doorgestuurd",
|
"continueRedirectingSubtitle": "Je wordt naar de app doorgestuurd",
|
||||||
"continueRedirectManually": "Redirect me manually",
|
"continueRedirectManually": "Stuur mij handmatig door",
|
||||||
"continueInsecureRedirectTitle": "Onveilige doorverwijzing",
|
"continueInsecureRedirectTitle": "Onveilige doorverwijzing",
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
|
"continueInsecureRedirectSubtitle": "Je probeert door te verwijzen van <code>https</code> naar <code>http</code> die niet veilig is. Weet je zeker dat je wilt doorgaan?",
|
||||||
"continueUntrustedRedirectTitle": "Untrusted redirect",
|
"continueUntrustedRedirectTitle": "Niet-vertrouwde doorverwijzing",
|
||||||
"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": "Je probeert door te sturen naar een domein dat niet overeenkomt met je geconfigureerde domein (<code>{{cookieDomain}}</code>). Weet je zeker dat je wilt doorgaan?",
|
||||||
"logoutFailTitle": "Afmelden mislukt",
|
"logoutFailTitle": "Afmelden mislukt",
|
||||||
"logoutFailSubtitle": "Probeer het opnieuw",
|
"logoutFailSubtitle": "Probeer het opnieuw",
|
||||||
"logoutSuccessTitle": "Afgemeld",
|
"logoutSuccessTitle": "Afgemeld",
|
||||||
"logoutSuccessSubtitle": "Je bent afgemeld",
|
"logoutSuccessSubtitle": "Je bent afgemeld",
|
||||||
"logoutTitle": "Afmelden",
|
"logoutTitle": "Afmelden",
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.",
|
"logoutUsernameSubtitle": "Je bent momenteel ingelogd als <code>{{username}}</code>. Klik op de onderstaande knop om uit te loggen.",
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.",
|
"logoutOauthSubtitle": "Je bent momenteel ingelogd als <code>{{username}}</code> met behulp van de {{provider}} OAuth provider. Klik op de onderstaande knop om uit te loggen.",
|
||||||
"notFoundTitle": "Pagina niet gevonden",
|
"notFoundTitle": "Pagina niet gevonden",
|
||||||
"notFoundSubtitle": "De pagina die je zoekt bestaat niet.",
|
"notFoundSubtitle": "De pagina die je zoekt bestaat niet.",
|
||||||
"notFoundButton": "Naar startpagina",
|
"notFoundButton": "Naar startpagina",
|
||||||
@@ -40,23 +40,42 @@
|
|||||||
"totpSuccessTitle": "Geverifiëerd",
|
"totpSuccessTitle": "Geverifiëerd",
|
||||||
"totpSuccessSubtitle": "Omleiden naar je app",
|
"totpSuccessSubtitle": "Omleiden naar je app",
|
||||||
"totpTitle": "Voer je TOTP-code in",
|
"totpTitle": "Voer je TOTP-code in",
|
||||||
"totpSubtitle": "Please enter the code from your authenticator app.",
|
"totpSubtitle": "Voer de code van je authenticator-app in.",
|
||||||
"unauthorizedTitle": "Ongeautoriseerd",
|
"unauthorizedTitle": "Ongeautoriseerd",
|
||||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
"unauthorizedResourceSubtitle": "De gebruiker met gebruikersnaam <code>{{username}}</code> is niet gemachtigd om de bron <code>{{resource}}</code> te gebruiken.",
|
||||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
"unauthorizedLoginSubtitle": "De gebruiker met gebruikersnaam <code>{{username}}</code> is niet gemachtigd om in te loggen.",
|
||||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
"unauthorizedGroupsSubtitle": "De gebruiker met gebruikersnaam <code>{{username}}</code> maakt geen deel uit van de groepen die vereist zijn door de bron <code>{{resource}}</code>.",
|
||||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
"unauthorizedIpSubtitle": "Jouw IP-adres <code>{{ip}}</code> is niet gemachtigd om de bron <code>{{resource}}</code> te gebruiken.",
|
||||||
"unauthorizedButton": "Opnieuw proberen",
|
"unauthorizedButton": "Opnieuw proberen",
|
||||||
"cancelTitle": "Cancel",
|
"cancelTitle": "Annuleren",
|
||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Wachtwoord vergeten?",
|
||||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
"failedToFetchProvidersTitle": "Fout bij het laden van de authenticatie-providers. Controleer je configuratie.",
|
||||||
"errorTitle": "An error occurred",
|
"errorTitle": "Er is een fout opgetreden",
|
||||||
|
"errorSubtitleInfo": "De volgende fout is opgetreden bij het verwerken van het verzoek:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "Je kunt je wachtwoord opnieuw instellen door de `USERS` omgevingsvariabele te wijzigen.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "Dit veld is verplicht",
|
||||||
"invalidInput": "Invalid input",
|
"invalidInput": "Ongeldige invoer",
|
||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Ongeldig domein",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "Deze instantie is geconfigureerd voor toegang tot <code>{{appUrl}}</code>, maar <code>{{currentUrl}}</code> wordt gebruikt. Als je doorgaat, kun je problemen ondervinden met authenticatie.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Negeren",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Ga naar het juiste domein",
|
||||||
}
|
"authorizeTitle": "Autoriseren",
|
||||||
|
"authorizeCardTitle": "Doorgaan naar {{app}}?",
|
||||||
|
"authorizeSubtitle": "Doorgaan naar deze app? Controleer de machtigingen die door de app worden gevraagd.",
|
||||||
|
"authorizeSubtitleOAuth": "Doorgaan naar deze app?",
|
||||||
|
"authorizeLoadingTitle": "Laden...",
|
||||||
|
"authorizeLoadingSubtitle": "Even geduld bij het laden van de cliëntinformatie.",
|
||||||
|
"authorizeSuccessTitle": "Geautoriseerd",
|
||||||
|
"authorizeSuccessSubtitle": "Je wordt binnen enkele seconden doorgestuurd naar de app.",
|
||||||
|
"authorizeErrorClientInfo": "Er is een fout opgetreden tijdens het laden van de cliëntinformatie. Probeer het later opnieuw.",
|
||||||
|
"authorizeErrorMissingParams": "De volgende parameters ontbreken: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Hiermee kan de app toegang krijgen tot jouw OpenID Connect-informatie.",
|
||||||
|
"emailScopeName": "E-mail",
|
||||||
|
"emailScopeDescription": "Hiermee kan de app toegang krijgen tot jouw e-mailadres.",
|
||||||
|
"profileScopeName": "Profiel",
|
||||||
|
"profileScopeDescription": "Hiermee kan de app toegang krijgen tot je profielinformatie.",
|
||||||
|
"groupsScopeName": "Groepen",
|
||||||
|
"groupsScopeDescription": "Hiermee kan de app toegang krijgen tot jouw groepsinformatie."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Forgot your password?",
|
||||||
"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:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Nie pamiętasz hasła?",
|
"forgotPasswordTitle": "Nie pamiętasz hasła?",
|
||||||
"failedToFetchProvidersTitle": "Nie udało się załadować dostawców uwierzytelniania. Sprawdź swoją konfigurację.",
|
"failedToFetchProvidersTitle": "Nie udało się załadować dostawców uwierzytelniania. Sprawdź swoją konfigurację.",
|
||||||
"errorTitle": "Wystąpił błąd",
|
"errorTitle": "Wystąpił błąd",
|
||||||
|
"errorSubtitleInfo": "Podczas przetwarzania żądania wystąpił następujący błąd:",
|
||||||
"errorSubtitle": "Wystąpił błąd podczas próby wykonania tej czynności. Sprawdź konsolę, aby uzyskać więcej informacji.",
|
"errorSubtitle": "Wystąpił błąd podczas próby wykonania tej czynności. Sprawdź konsolę, aby uzyskać więcej informacji.",
|
||||||
"forgotPasswordMessage": "Możesz zresetować hasło, zmieniając zmienną środowiskową `USERS`.",
|
"forgotPasswordMessage": "Możesz zresetować hasło, zmieniając zmienną środowiskową `USERS`.",
|
||||||
"fieldRequired": "To pole jest wymagane",
|
"fieldRequired": "To pole jest wymagane",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Nieprawidłowa domena",
|
"domainWarningTitle": "Nieprawidłowa domena",
|
||||||
"domainWarningSubtitle": "Ta instancja jest skonfigurowana do uzyskania dostępu z <code>{{appUrl}}</code>, ale <code>{{currentUrl}}</code> jest w użyciu. Jeśli będziesz kontynuować, mogą wystąpić problemy z uwierzytelnianiem.",
|
"domainWarningSubtitle": "Ta instancja jest skonfigurowana do uzyskania dostępu z <code>{{appUrl}}</code>, ale <code>{{currentUrl}}</code> jest w użyciu. Jeśli będziesz kontynuować, mogą wystąpić problemy z uwierzytelnianiem.",
|
||||||
"ignoreTitle": "Zignoruj",
|
"ignoreTitle": "Zignoruj",
|
||||||
"goToCorrectDomainTitle": "Przejdź do prawidłowej domeny"
|
"goToCorrectDomainTitle": "Przejdź do prawidłowej domeny",
|
||||||
}
|
"authorizeTitle": "Autoryzuj",
|
||||||
|
"authorizeCardTitle": "Kontynuować do {{app}}?",
|
||||||
|
"authorizeSubtitle": "Czy chcesz kontynuować do tej aplikacji? Uważnie zapoznaj się z uprawnieniami żądanymi przez aplikację.",
|
||||||
|
"authorizeSubtitleOAuth": "Czy chcesz kontynuować do tej aplikacji?",
|
||||||
|
"authorizeLoadingTitle": "Wczytywanie...",
|
||||||
|
"authorizeLoadingSubtitle": "Proszę czekać, aż załadujemy informacje o kliencie.",
|
||||||
|
"authorizeSuccessTitle": "Autoryzowano",
|
||||||
|
"authorizeSuccessSubtitle": "Za kilka sekund nastąpi przekierowanie do aplikacji.",
|
||||||
|
"authorizeErrorClientInfo": "Wystąpił błąd podczas ładowania informacji o kliencie. Spróbuj ponownie później.",
|
||||||
|
"authorizeErrorMissingParams": "Brakuje następujących parametrów: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Zezwala aplikacji na dostęp do informacji o OpenID Connect.",
|
||||||
|
"emailScopeName": "E-mail",
|
||||||
|
"emailScopeDescription": "Zezwala aplikacji na dostęp do adresów e-mail.",
|
||||||
|
"profileScopeName": "Profil",
|
||||||
|
"profileScopeDescription": "Zezwala aplikacji na dostęp do informacji o porfilu.",
|
||||||
|
"groupsScopeName": "Grupy",
|
||||||
|
"groupsScopeDescription": "Zezwala aplikacji na dostęp do informacji o grupie."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Esqueceu sua senha?",
|
"forgotPasswordTitle": "Esqueceu sua senha?",
|
||||||
"failedToFetchProvidersTitle": "Falha ao carregar provedores de autenticação. Verifique sua configuração.",
|
"failedToFetchProvidersTitle": "Falha ao carregar provedores de autenticação. Verifique sua configuração.",
|
||||||
"errorTitle": "Ocorreu um erro",
|
"errorTitle": "Ocorreu um erro",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "Ocorreu um erro ao tentar executar esta ação. Por favor, verifique o console para mais informações.",
|
"errorSubtitle": "Ocorreu um erro ao tentar executar esta ação. Por favor, verifique o console para mais informações.",
|
||||||
"forgotPasswordMessage": "Você pode redefinir sua senha alterando a variável de ambiente `USERS`.",
|
"forgotPasswordMessage": "Você pode redefinir sua senha alterando a variável de ambiente `USERS`.",
|
||||||
"fieldRequired": "Este campo é obrigatório",
|
"fieldRequired": "Este campo é obrigatório",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Domínio inválido",
|
"domainWarningTitle": "Domínio inválido",
|
||||||
"domainWarningSubtitle": "Esta instância está configurada para ser acessada de <code>{{appUrl}}</code>, mas <code>{{currentUrl}}</code> está sendo usado. Se você continuar, você pode encontrar problemas com a autenticação.",
|
"domainWarningSubtitle": "Esta instância está configurada para ser acessada de <code>{{appUrl}}</code>, mas <code>{{currentUrl}}</code> está sendo usado. Se você continuar, você pode encontrar problemas com a autenticação.",
|
||||||
"ignoreTitle": "Ignorar",
|
"ignoreTitle": "Ignorar",
|
||||||
"goToCorrectDomainTitle": "Ir para o domínio correto"
|
"goToCorrectDomainTitle": "Ir para o domínio correto",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,62 +1,81 @@
|
|||||||
{
|
{
|
||||||
"loginTitle": "Welcome back, login with",
|
"loginTitle": "Bem-vindo de volta, inicia sessão com",
|
||||||
"loginTitleSimple": "Welcome back, please login",
|
"loginTitleSimple": "Bem-vindo de volta, inicia sessão",
|
||||||
"loginDivider": "Or",
|
"loginDivider": "Ou",
|
||||||
"loginUsername": "Username",
|
"loginUsername": "Nome de utilizador",
|
||||||
"loginPassword": "Password",
|
"loginPassword": "Palavra-passe",
|
||||||
"loginSubmit": "Login",
|
"loginSubmit": "Iniciar sessão",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Falha ao iniciar sessão",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Verifica o nome de utilizador e a palavra-passe",
|
||||||
"loginFailRateLimit": "You failed to login too many times. Please try again later",
|
"loginFailRateLimit": "Falhaste o início de sessão demasiadas vezes. Tenta novamente mais tarde",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Sessão iniciada",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Bem-vindo de volta!",
|
||||||
"loginOauthFailTitle": "An error occurred",
|
"loginOauthFailTitle": "Ocorreu um erro",
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
"loginOauthFailSubtitle": "Não foi possível obter o URL OAuth",
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
"loginOauthSuccessTitle": "A redirecionar",
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
"loginOauthSuccessSubtitle": "A redirecionar para o teu fornecedor OAuth",
|
||||||
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
|
"loginOauthAutoRedirectTitle": "Redirecionamento automático OAuth",
|
||||||
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
|
"loginOauthAutoRedirectSubtitle": "Vais ser redirecionado automaticamente para o teu fornecedor OAuth para autenticação.",
|
||||||
"loginOauthAutoRedirectButton": "Redirect now",
|
"loginOauthAutoRedirectButton": "Redirecionar agora",
|
||||||
"continueTitle": "Continue",
|
"continueTitle": "Continuar",
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
"continueRedirectingTitle": "A redirecionar...",
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
"continueRedirectingSubtitle": "Deverás ser redirecionado para a aplicação em breve",
|
||||||
"continueRedirectManually": "Redirect me manually",
|
"continueRedirectManually": "Redirecionar manualmente",
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
"continueInsecureRedirectTitle": "Redirecionamento inseguro",
|
||||||
"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": "Estás a tentar redirecionar de <code>https</code> para <code>http</code>, o que não é seguro. Tens a certeza de que queres continuar?",
|
||||||
"continueUntrustedRedirectTitle": "Untrusted redirect",
|
"continueUntrustedRedirectTitle": "Redirecionamento não fidedigno",
|
||||||
"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": "Estás a tentar redirecionar para um domínio que não corresponde ao domínio configurado (<code>{{cookieDomain}}</code>). Tens a certeza de que queres continuar?",
|
||||||
"logoutFailTitle": "Failed to log out",
|
"logoutFailTitle": "Falha ao terminar sessão",
|
||||||
"logoutFailSubtitle": "Please try again",
|
"logoutFailSubtitle": "Tenta novamente",
|
||||||
"logoutSuccessTitle": "Logged out",
|
"logoutSuccessTitle": "Sessão terminada",
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
"logoutSuccessSubtitle": "Terminaste a sessão com sucesso",
|
||||||
"logoutTitle": "Logout",
|
"logoutTitle": "Terminar sessão",
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.",
|
"logoutUsernameSubtitle": "Estás com sessão iniciada como <code>{{username}}</code>. Clica no botão abaixo para terminar sessão.",
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.",
|
"logoutOauthSubtitle": "Estás com sessão iniciada como <code>{{username}}</code> através do fornecedor OAuth {{provider}}. Clica no botão abaixo para terminar sessão.",
|
||||||
"notFoundTitle": "Page not found",
|
"notFoundTitle": "Página não encontrada",
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
"notFoundSubtitle": "A página que procuras não existe.",
|
||||||
"notFoundButton": "Go home",
|
"notFoundButton": "Ir para o início",
|
||||||
"totpFailTitle": "Failed to verify code",
|
"totpFailTitle": "Falha na verificação do código",
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
"totpFailSubtitle": "Verifica o código e tenta novamente",
|
||||||
"totpSuccessTitle": "Verified",
|
"totpSuccessTitle": "Verificado",
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "A redirecionar para a tua aplicação",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Introduz o teu código TOTP",
|
||||||
"totpSubtitle": "Please enter the code from your authenticator app.",
|
"totpSubtitle": "Introduz o código da tua aplicação de autenticação.",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Não autorizado",
|
||||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
"unauthorizedResourceSubtitle": "O utilizador com o nome <code>{{username}}</code> não tem autorização para aceder ao recurso <code>{{resource}}</code>.",
|
||||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
"unauthorizedLoginSubtitle": "O utilizador com o nome <code>{{username}}</code> não tem autorização para iniciar sessão.",
|
||||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
"unauthorizedGroupsSubtitle": "O utilizador com o nome <code>{{username}}</code> não pertence aos grupos exigidos pelo recurso <code>{{resource}}</code>.",
|
||||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
"unauthorizedIpSubtitle": "O teu endereço IP <code>{{ip}}</code> não tem autorização para aceder ao recurso <code>{{resource}}</code>.",
|
||||||
"unauthorizedButton": "Try again",
|
"unauthorizedButton": "Tentar novamente",
|
||||||
"cancelTitle": "Cancel",
|
"cancelTitle": "Cancelar",
|
||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Esqueceste-te da palavra-passe?",
|
||||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
"failedToFetchProvidersTitle": "Falha ao carregar os fornecedores de autenticação. Verifica a configuração.",
|
||||||
"errorTitle": "An error occurred",
|
"errorTitle": "Ocorreu um erro",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"errorSubtitle": "Ocorreu um erro ao tentar executar esta ação. Consulta a consola para mais informações.",
|
||||||
"fieldRequired": "This field is required",
|
"forgotPasswordMessage": "Podes redefinir a tua palavra-passe alterando a variável de ambiente `USERS`.",
|
||||||
"invalidInput": "Invalid input",
|
"fieldRequired": "Este campo é obrigatório",
|
||||||
"domainWarningTitle": "Invalid Domain",
|
"invalidInput": "Entrada inválida",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningTitle": "Domínio inválido",
|
||||||
"ignoreTitle": "Ignore",
|
"domainWarningSubtitle": "Esta instância está configurada para ser acedida a partir de <code>{{appUrl}}</code>, mas está a ser usado <code>{{currentUrl}}</code>. Se continuares, poderás ter problemas de autenticação.",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"ignoreTitle": "Ignorar",
|
||||||
}
|
"goToCorrectDomainTitle": "Ir para o domínio correto",
|
||||||
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Forgot your password?",
|
||||||
"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:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Забыли пароль?",
|
"forgotPasswordTitle": "Забыли пароль?",
|
||||||
"failedToFetchProvidersTitle": "Не удалось загрузить поставщика авторизации. Пожалуйста, проверьте конфигурацию.",
|
"failedToFetchProvidersTitle": "Не удалось загрузить поставщика авторизации. Пожалуйста, проверьте конфигурацию.",
|
||||||
"errorTitle": "Произошла ошибка",
|
"errorTitle": "Произошла ошибка",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "Произошла ошибка при попытке выполнить это действие. Проверьте консоль для дополнительной информации.",
|
"errorSubtitle": "Произошла ошибка при попытке выполнить это действие. Проверьте консоль для дополнительной информации.",
|
||||||
"forgotPasswordMessage": "Вы можете сбросить свой пароль, изменив переменную окружения `USERS`.",
|
"forgotPasswordMessage": "Вы можете сбросить свой пароль, изменив переменную окружения `USERS`.",
|
||||||
"fieldRequired": "Это поле является обязательным",
|
"fieldRequired": "Это поле является обязательным",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Неверный домен",
|
"domainWarningTitle": "Неверный домен",
|
||||||
"domainWarningSubtitle": "Этот экземпляр настроен на доступ к нему из <code>{{appUrl}}</code>, но <code>{{currentUrl}}</code> в настоящее время используется. Если вы продолжите, то могут возникнуть проблемы с авторизацией.",
|
"domainWarningSubtitle": "Этот экземпляр настроен на доступ к нему из <code>{{appUrl}}</code>, но <code>{{currentUrl}}</code> в настоящее время используется. Если вы продолжите, то могут возникнуть проблемы с авторизацией.",
|
||||||
"ignoreTitle": "Игнорировать",
|
"ignoreTitle": "Игнорировать",
|
||||||
"goToCorrectDomainTitle": "Перейти к правильному домену"
|
"goToCorrectDomainTitle": "Перейти к правильному домену",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Заборавили сте лозинку?",
|
"forgotPasswordTitle": "Заборавили сте лозинку?",
|
||||||
"failedToFetchProvidersTitle": "Није успело учитавање провајдера аутентификације. Молим вас проверите ваша подешавања.",
|
"failedToFetchProvidersTitle": "Није успело учитавање провајдера аутентификације. Молим вас проверите ваша подешавања.",
|
||||||
"errorTitle": "Појавила се грешка",
|
"errorTitle": "Појавила се грешка",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "Појавила се грешка при покушају извршавања ове радње. Молим вас проверите конзолу за додатне информације.",
|
"errorSubtitle": "Појавила се грешка при покушају извршавања ове радње. Молим вас проверите конзолу за додатне информације.",
|
||||||
"forgotPasswordMessage": "Можете поништити вашу лозинку променом `USERS` променљиве окружења.",
|
"forgotPasswordMessage": "Можете поништити вашу лозинку променом `USERS` променљиве окружења.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Forgot your password?",
|
||||||
"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:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,62 +1,81 @@
|
|||||||
{
|
{
|
||||||
"loginTitle": "Welcome back, login with",
|
"loginTitle": "Tekrar Hoş Geldiniz, giriş yapın",
|
||||||
"loginTitleSimple": "Welcome back, please login",
|
"loginTitleSimple": "Tekrar hoş geldiniz, lütfen giriş yapın",
|
||||||
"loginDivider": "Or",
|
"loginDivider": "Ya da",
|
||||||
"loginUsername": "Kullanıcı Adı",
|
"loginUsername": "Kullanıcı Adı",
|
||||||
"loginPassword": "Şifre",
|
"loginPassword": "Şifre",
|
||||||
"loginSubmit": "Giriş Yap",
|
"loginSubmit": "Giriş Yap",
|
||||||
"loginFailTitle": "Giriş yapılamadı",
|
"loginFailTitle": "Giriş yapılamadı",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Lütfen kullanıcı adınızı ve şifrenizi kontrol edin",
|
||||||
"loginFailRateLimit": "You failed to login too many times. Please try again later",
|
"loginFailRateLimit": "Çok fazla kez giriş yapma girişiminde bulundunuz. Lütfen daha sonra tekrar deneyin",
|
||||||
"loginSuccessTitle": "Giriş yapıldı",
|
"loginSuccessTitle": "Giriş yapıldı",
|
||||||
"loginSuccessSubtitle": "Tekrar hoş geldiniz!",
|
"loginSuccessSubtitle": "Tekrar hoş geldiniz!",
|
||||||
"loginOauthFailTitle": "An error occurred",
|
"loginOauthFailTitle": "Hata oluştu",
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
"loginOauthFailSubtitle": "OAuth URL'si alınamadı",
|
||||||
"loginOauthSuccessTitle": "Yönlendiriliyor",
|
"loginOauthSuccessTitle": "Yönlendiriliyor",
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
"loginOauthSuccessSubtitle": "OAuth sağlayıcınıza yönlendiriliyor",
|
||||||
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
|
"loginOauthAutoRedirectTitle": "OAuth Otomatik Yönlendirme",
|
||||||
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
|
"loginOauthAutoRedirectSubtitle": "Kimlik doğrulama işlemi için otomatik olarak OAuth sağlayıcınıza yönlendirileceksiniz.",
|
||||||
"loginOauthAutoRedirectButton": "Redirect now",
|
"loginOauthAutoRedirectButton": "Şimdi Yönlendir",
|
||||||
"continueTitle": "Devam et",
|
"continueTitle": "Devam et",
|
||||||
"continueRedirectingTitle": "Yönlendiriliyor...",
|
"continueRedirectingTitle": "Yönlendiriliyor...",
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
"continueRedirectingSubtitle": "Kısa süre içinde uygulamaya yönlendirileceksiniz",
|
||||||
"continueRedirectManually": "Redirect me manually",
|
"continueRedirectManually": "Beni manuel olarak yönlendir",
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
"continueInsecureRedirectTitle": "Güvenli olmayan yönlendirme",
|
||||||
"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": "<code>http</code> adresinden <code>http</code> adresine yönlendirme yapmaya çalışıyorsunuz, bu güvenli değil. Devam etmek istediğinizden emin misiniz?",
|
||||||
"continueUntrustedRedirectTitle": "Untrusted redirect",
|
"continueUntrustedRedirectTitle": "Güvenilmeyen yönlendirme",
|
||||||
"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": "Yapılandırdığınız alan adıyla eşleşmeyen bir alana yönlendirme yapmaya çalışıyorsunuz (<code>{{cookieDomain}}</code>). Devam etmek istediğinize emin misiniz?",
|
||||||
"logoutFailTitle": "Failed to log out",
|
"logoutFailTitle": "Çıkış Yapılamadı",
|
||||||
"logoutFailSubtitle": "Lütfen tekrar deneyin",
|
"logoutFailSubtitle": "Lütfen tekrar deneyin",
|
||||||
"logoutSuccessTitle": "Çıkış yapıldı",
|
"logoutSuccessTitle": "Çıkış yapıldı",
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
"logoutSuccessSubtitle": "Çıkış yaptınız",
|
||||||
"logoutTitle": "Logout",
|
"logoutTitle": "Çıkış yap",
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.",
|
"logoutUsernameSubtitle": "<code>{{username}}</code> olarak giriş yapmış durumdasınız. Çıkış yapmak için aşağıdaki düğmeye tıklayın.",
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.",
|
"logoutOauthSubtitle": "Şu anda {{provider}} OAuth sağlayıcısını kullanarak <code>{{username}}</code> olarak oturum açmış durumdasınız. Oturumunuzu kapatmak için aşağıdaki düğmeye tıklayın.",
|
||||||
"notFoundTitle": "Sayfa bulunamadı",
|
"notFoundTitle": "Sayfa bulunamadı",
|
||||||
"notFoundSubtitle": "Aradığınız sayfa mevcut değil.",
|
"notFoundSubtitle": "Aradığınız sayfa mevcut değil.",
|
||||||
"notFoundButton": "Ana sayfaya git",
|
"notFoundButton": "Ana sayfaya git",
|
||||||
"totpFailTitle": "Kod doğrulanamadı",
|
"totpFailTitle": "Kod doğrulanamadı",
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
"totpFailSubtitle": "Lütfen kodunuzu kontrol edin ve tekrar deneyin",
|
||||||
"totpSuccessTitle": "Doğrulandı",
|
"totpSuccessTitle": "Doğrulandı",
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Uygulamanıza yönlendiriliyor",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "TOTP kodunuzu girin",
|
||||||
"totpSubtitle": "Please enter the code from your authenticator app.",
|
"totpSubtitle": "Lütfen kimlik doğrulama uygulamanızdan aldığınız kodu girin.",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Yetkisiz",
|
||||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
"unauthorizedResourceSubtitle": "Kullanıcı adı <code>{{username}}</code> olan kullanıcının <code>{{resource}}</code> kaynağına erişim yetkisi bulunmamaktadır.",
|
||||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
"unauthorizedLoginSubtitle": "Kullanıcı adı <code>{{username}}</code> olan kullanıcının oturum açma yetkisi yok.",
|
||||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
"unauthorizedGroupsSubtitle": "Kullanıcı adı <code>{{username}}</code> olan kullanıcı, <code>{{resource}}</code> kaynağının gerektirdiği gruplarda bulunmuyor.",
|
||||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
"unauthorizedIpSubtitle": "IP adresiniz <code>{{ip}}</code>, <code>{{resource}}</code> kaynağına erişim yetkisine sahip değil.",
|
||||||
"unauthorizedButton": "Try again",
|
"unauthorizedButton": "Tekrar deneyin",
|
||||||
"cancelTitle": "İptal",
|
"cancelTitle": "İptal",
|
||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Şifrenizi mi unuttunuz?",
|
||||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
"failedToFetchProvidersTitle": "Kimlik doğrulama sağlayıcıları yüklenemedi. Lütfen yapılandırmanızı kontrol edin.",
|
||||||
"errorTitle": "An error occurred",
|
"errorTitle": "Bir hata oluştu",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "Parolanızı `USERS` ortam değişkenini değiştirerek sıfırlayabilirsiniz.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "Bu alan zorunludur",
|
||||||
"invalidInput": "Invalid input",
|
"invalidInput": "Geçersiz girdi",
|
||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Geçersiz alan adı",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "Bu örnek, <code>{{appUrl}}</code> adresinden erişilecek şekilde yapılandırılmıştır, ancak <code>{{currentUrl}}</code> kullanılmaktadır. Devam ederseniz, kimlik doğrulama ile ilgili sorunlarla karşılaşabilirsiniz.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Yoksay",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Doğru alana gidin",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,62 +1,81 @@
|
|||||||
{
|
{
|
||||||
"loginTitle": "З поверненням, увійдіть через",
|
"loginTitle": "З поверненням, увійдіть через",
|
||||||
"loginTitleSimple": "Welcome back, please login",
|
"loginTitleSimple": "З поверненням, будь ласка, авторизуйтесь",
|
||||||
"loginDivider": "Or",
|
"loginDivider": "Або",
|
||||||
"loginUsername": "Username",
|
"loginUsername": "Ім'я користувача",
|
||||||
"loginPassword": "Password",
|
"loginPassword": "Пароль",
|
||||||
"loginSubmit": "Login",
|
"loginSubmit": "Увійти",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Не вдалося авторизуватися",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Перевірте ім'я користувача та пароль",
|
||||||
"loginFailRateLimit": "You failed to login too many times. Please try again later",
|
"loginFailRateLimit": "Ви не змогли увійти занадто багато разів. Будь ласка, спробуйте ще раз пізніше",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Вхід здійснено",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "З поверненням!",
|
||||||
"loginOauthFailTitle": "An error occurred",
|
"loginOauthFailTitle": "Виникла помилка",
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
"loginOauthFailSubtitle": "Не вдалося отримати OAuth URL",
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
"loginOauthSuccessTitle": "Перенаправляємо",
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
"loginOauthSuccessSubtitle": "Перенаправляємо до вашого провайдера OAuth",
|
||||||
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
|
"loginOauthAutoRedirectTitle": "Автоматичне переспрямування OAuth",
|
||||||
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
|
"loginOauthAutoRedirectSubtitle": "Ви будете автоматично перенаправлені до вашого провайдера OAuth для автентифікації.",
|
||||||
"loginOauthAutoRedirectButton": "Redirect now",
|
"loginOauthAutoRedirectButton": "Перейти зараз",
|
||||||
"continueTitle": "Continue",
|
"continueTitle": "Продовжити",
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
"continueRedirectingTitle": "Перенаправлення...",
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
"continueRedirectingSubtitle": "Незабаром ви будете перенаправлені в додаток",
|
||||||
"continueRedirectManually": "Redirect me manually",
|
"continueRedirectManually": "Перенаправити мене вручну",
|
||||||
"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": "Ви намагаєтесь перенаправити з <code>https</code> на <code>http</code> який не є безпечним. Ви впевнені, що хочете продовжити?",
|
||||||
"continueUntrustedRedirectTitle": "Untrusted redirect",
|
"continueUntrustedRedirectTitle": "Недовірене перенаправлення",
|
||||||
"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": "Ви намагаєтесь перенаправити на домен, який не збігається з вашим налаштованим доменом (<code>{{cookieDomain}}</code>). Впевнені, що хочете продовжити?",
|
||||||
"logoutFailTitle": "Failed to log out",
|
"logoutFailTitle": "Не вдалося вийти",
|
||||||
"logoutFailSubtitle": "Please try again",
|
"logoutFailSubtitle": "Будь ласка, спробуйте знову",
|
||||||
"logoutSuccessTitle": "Logged out",
|
"logoutSuccessTitle": "Ви вийшли",
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
"logoutSuccessSubtitle": "Ви вийшли з системи",
|
||||||
"logoutTitle": "Logout",
|
"logoutTitle": "Вийти",
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.",
|
"logoutUsernameSubtitle": "Зараз ви увійшли як <code>{{username}}</code>. Натисніть кнопку нижче для виходу.",
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.",
|
"logoutOauthSubtitle": "Наразі ви увійшли як <code>{{username}}</code> використовуючи провайдера {{provider}} OAuth. Натисніть кнопку нижче, щоб вийти.",
|
||||||
"notFoundTitle": "Page not found",
|
"notFoundTitle": "Сторінку не знайдено",
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
"notFoundSubtitle": "Сторінка, яку ви шукаєте, не існує.",
|
||||||
"notFoundButton": "Go home",
|
"notFoundButton": "На головну",
|
||||||
"totpFailTitle": "Failed to verify code",
|
"totpFailTitle": "Не вдалося перевірити код",
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
"totpFailSubtitle": "Перевірте ваш код і спробуйте ще раз",
|
||||||
"totpSuccessTitle": "Verified",
|
"totpSuccessTitle": "Перевірено",
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Перенаправлення до вашого додатку",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Введіть ваш TOTP код",
|
||||||
"totpSubtitle": "Please enter the code from your authenticator app.",
|
"totpSubtitle": "Будь ласка, введіть код з вашого додатку для автентифікації.",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Доступ обмежено",
|
||||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
"unauthorizedResourceSubtitle": "Користувач з ім'ям користувача <code>{{username}}</code> не має права доступу до ресурсу <code>{{resource}}</code>.",
|
||||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
"unauthorizedLoginSubtitle": "Користувач з іменем <code>{{username}}</code> не авторизований для входу.",
|
||||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
"unauthorizedGroupsSubtitle": "Користувач з іменем <code>{{username}}</code> не входить до груп, що необхідні для ресурсу <code>{{resource}}</code>.",
|
||||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
"unauthorizedIpSubtitle": "Ваша IP-адреса <code>{{ip}}</code> не авторизована для доступу до ресурсу <code>{{resource}}</code>.",
|
||||||
"unauthorizedButton": "Try again",
|
"unauthorizedButton": "Спробуйте ще раз",
|
||||||
"cancelTitle": "Cancel",
|
"cancelTitle": "Скасовувати",
|
||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Забули пароль?",
|
||||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
"failedToFetchProvidersTitle": "Не вдалося завантажити провайдерів автентифікації. Будь ласка, перевірте вашу конфігурацію.",
|
||||||
"errorTitle": "An error occurred",
|
"errorTitle": "Виникла помилка",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "Ви можете скинути пароль, змінивши змінну середовища \"USERS\".",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "Це поле обов'язкове для заповнення",
|
||||||
"invalidInput": "Invalid input",
|
"invalidInput": "Невірне введення",
|
||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Невірний домен",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "Даний ресурс налаштований для доступу з <code>{{appUrl}}</code>, але використовується <code>{{currentUrl}}</code>. Якщо ви продовжите, можуть виникнути проблеми з автентифікацією.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ігнорувати",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Перейти за коректним доменом",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Bạn quên mật khẩu?",
|
"forgotPasswordTitle": "Bạn quên mật khẩu?",
|
||||||
"failedToFetchProvidersTitle": "Không tải được nhà cung cấp xác thực. Vui lòng kiểm tra cấu hình của bạn.",
|
"failedToFetchProvidersTitle": "Không tải được nhà cung cấp xác thực. Vui lòng kiểm tra cấu hình của bạn.",
|
||||||
"errorTitle": "An error occurred",
|
"errorTitle": "An error occurred",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "Đã xảy ra lỗi khi thực hiện thao tác này. Vui lòng kiểm tra bảng điều khiển để biết thêm thông tin.",
|
"errorSubtitle": "Đã xảy ra lỗi khi thực hiện thao tác này. Vui lòng kiểm tra bảng điều khiển để biết thêm thông tin.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "忘记密码?",
|
"forgotPasswordTitle": "忘记密码?",
|
||||||
"failedToFetchProvidersTitle": "加载身份验证提供程序失败,请检查您的配置。",
|
"failedToFetchProvidersTitle": "加载身份验证提供程序失败,请检查您的配置。",
|
||||||
"errorTitle": "发生了错误",
|
"errorTitle": "发生了错误",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "执行此操作时发生错误,请检查控制台以获取更多信息。",
|
"errorSubtitle": "执行此操作时发生错误,请检查控制台以获取更多信息。",
|
||||||
"forgotPasswordMessage": "您可以通过更改 `USERS ` 环境变量重置您的密码。",
|
"forgotPasswordMessage": "您可以通过更改 `USERS ` 环境变量重置您的密码。",
|
||||||
"fieldRequired": "必添字段",
|
"fieldRequired": "必添字段",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "无效域名",
|
"domainWarningTitle": "无效域名",
|
||||||
"domainWarningSubtitle": "当前实例配置的访问地址为 <code>{{appUrl}}</code>,但您正在使用 <code>{{currentUrl}}</code>。若继续操作,可能会遇到身份验证问题。",
|
"domainWarningSubtitle": "当前实例配置的访问地址为 <code>{{appUrl}}</code>,但您正在使用 <code>{{currentUrl}}</code>。若继续操作,可能会遇到身份验证问题。",
|
||||||
"ignoreTitle": "忽略",
|
"ignoreTitle": "忽略",
|
||||||
"goToCorrectDomainTitle": "转到正确的域名"
|
"goToCorrectDomainTitle": "转到正确的域名",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "忘記密碼?",
|
"forgotPasswordTitle": "忘記密碼?",
|
||||||
"failedToFetchProvidersTitle": "載入驗證供應商失敗。請檢查您的設定。",
|
"failedToFetchProvidersTitle": "載入驗證供應商失敗。請檢查您的設定。",
|
||||||
"errorTitle": "發生錯誤",
|
"errorTitle": "發生錯誤",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "執行此操作時發生錯誤。請檢查主控台以獲取更多資訊。",
|
"errorSubtitle": "執行此操作時發生錯誤。請檢查主控台以獲取更多資訊。",
|
||||||
"forgotPasswordMessage": "透過修改 `USERS` 環境變數,你可以重設你的密碼。",
|
"forgotPasswordMessage": "透過修改 `USERS` 環境變數,你可以重設你的密碼。",
|
||||||
"fieldRequired": "此為必填欄位",
|
"fieldRequired": "此為必填欄位",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "無效的網域",
|
"domainWarningTitle": "無效的網域",
|
||||||
"domainWarningSubtitle": "此服務設定為透過 <code>{{appUrl}}</code> 存取,但目前使用的是 <code>{{currentUrl}}</code>。若繼續操作,可能會遇到驗證問題。",
|
"domainWarningSubtitle": "此服務設定為透過 <code>{{appUrl}}</code> 存取,但目前使用的是 <code>{{currentUrl}}</code>。若繼續操作,可能會遇到驗證問題。",
|
||||||
"ignoreTitle": "忽略",
|
"ignoreTitle": "忽略",
|
||||||
"goToCorrectDomainTitle": "前往正確域名"
|
"goToCorrectDomainTitle": "前往正確域名",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,15 +5,6 @@ export function cn(...inputs: ClassValue[]) {
|
|||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isValidUrl = (url: string) => {
|
|
||||||
try {
|
|
||||||
new URL(url);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const capitalize = (str: string) => {
|
export const capitalize = (str: string) => {
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { AppContextProvider } from "./context/app-context.tsx";
|
|||||||
import { UserContextProvider } from "./context/user-context.tsx";
|
import { UserContextProvider } from "./context/user-context.tsx";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { ThemeProvider } from "./components/providers/theme-provider.tsx";
|
import { ThemeProvider } from "./components/providers/theme-provider.tsx";
|
||||||
|
import { AuthorizePage } from "./pages/authorize-page.tsx";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ createRoot(document.getElementById("root")!).render(
|
|||||||
<Route element={<Layout />} errorElement={<ErrorPage />}>
|
<Route element={<Layout />} errorElement={<ErrorPage />}>
|
||||||
<Route path="/" element={<App />} />
|
<Route path="/" element={<App />} />
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
|
<Route path="/authorize" element={<AuthorizePage />} />
|
||||||
<Route path="/logout" element={<LogoutPage />} />
|
<Route path="/logout" element={<LogoutPage />} />
|
||||||
<Route path="/continue" element={<ContinuePage />} />
|
<Route path="/continue" element={<ContinuePage />} />
|
||||||
<Route path="/totp" element={<TotpPage />} />
|
<Route path="/totp" element={<TotpPage />} />
|
||||||
|
|||||||
199
frontend/src/pages/authorize-page.tsx
Normal file
199
frontend/src/pages/authorize-page.tsx
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import { useUserContext } from "@/context/user-context";
|
||||||
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
import { Navigate, useNavigate } from "react-router";
|
||||||
|
import { useLocation } from "react-router";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardContent,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { getOidcClientInfoSchema } from "@/schemas/oidc-schemas";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import axios from "axios";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { useOIDCParams } from "@/lib/hooks/oidc";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { TFunction } from "i18next";
|
||||||
|
import { Mail, Shield, User, Users } from "lucide-react";
|
||||||
|
|
||||||
|
type Scope = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const scopeMapIconProps = {
|
||||||
|
className: "stroke-card stroke-2.5",
|
||||||
|
};
|
||||||
|
|
||||||
|
const createScopeMap = (t: TFunction<"translation", undefined>): Scope[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "openid",
|
||||||
|
name: t("openidScopeName"),
|
||||||
|
description: t("openidScopeDescription"),
|
||||||
|
icon: <Shield {...scopeMapIconProps} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "email",
|
||||||
|
name: t("emailScopeName"),
|
||||||
|
description: t("emailScopeDescription"),
|
||||||
|
icon: <Mail {...scopeMapIconProps} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "profile",
|
||||||
|
name: t("profileScopeName"),
|
||||||
|
description: t("profileScopeDescription"),
|
||||||
|
icon: <User {...scopeMapIconProps} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "groups",
|
||||||
|
name: t("groupsScopeName"),
|
||||||
|
description: t("groupsScopeDescription"),
|
||||||
|
icon: <Users {...scopeMapIconProps} />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AuthorizePage = () => {
|
||||||
|
const { isLoggedIn } = useUserContext();
|
||||||
|
const { search } = useLocation();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const scopeMap = createScopeMap(t);
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams(search);
|
||||||
|
const {
|
||||||
|
values: props,
|
||||||
|
missingParams,
|
||||||
|
isOidc,
|
||||||
|
compiled: compiledOIDCParams,
|
||||||
|
} = useOIDCParams(searchParams);
|
||||||
|
const scopes = props.scope ? props.scope.split(" ").filter(Boolean) : [];
|
||||||
|
|
||||||
|
const getClientInfo = useQuery({
|
||||||
|
queryKey: ["client", props.client_id],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await fetch(`/api/oidc/clients/${props.client_id}`);
|
||||||
|
const data = await getOidcClientInfoSchema.parseAsync(await res.json());
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
enabled: isOidc,
|
||||||
|
});
|
||||||
|
|
||||||
|
const authorizeMutation = useMutation({
|
||||||
|
mutationFn: () => {
|
||||||
|
return axios.post("/api/oidc/authorize", {
|
||||||
|
scope: props.scope,
|
||||||
|
response_type: props.response_type,
|
||||||
|
client_id: props.client_id,
|
||||||
|
redirect_uri: props.redirect_uri,
|
||||||
|
state: props.state,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
mutationKey: ["authorize", props.client_id],
|
||||||
|
onSuccess: (data) => {
|
||||||
|
toast.info(t("authorizeSuccessTitle"), {
|
||||||
|
description: t("authorizeSuccessSubtitle"),
|
||||||
|
});
|
||||||
|
window.location.replace(data.data.redirect_uri);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
window.location.replace(
|
||||||
|
`/error?error=${encodeURIComponent(error.message)}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (missingParams.length > 0) {
|
||||||
|
return (
|
||||||
|
<Navigate
|
||||||
|
to={`/error?error=${encodeURIComponent(t("authorizeErrorMissingParams", { missingParams: missingParams.join(", ") }))}`}
|
||||||
|
replace
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
return <Navigate to={`/login?${compiledOIDCParams}`} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getClientInfo.isLoading) {
|
||||||
|
return (
|
||||||
|
<Card className="min-w-xs sm:min-w-sm">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-3xl">
|
||||||
|
{t("authorizeLoadingTitle")}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>{t("authorizeLoadingSubtitle")}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getClientInfo.isError) {
|
||||||
|
return (
|
||||||
|
<Navigate
|
||||||
|
to={`/error?error=${encodeURIComponent(t("authorizeErrorClientInfo"))}`}
|
||||||
|
replace
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="min-w-xs sm:min-w-sm mx-4">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-3xl">
|
||||||
|
{t("authorizeCardTitle", {
|
||||||
|
app: getClientInfo.data?.name || "Unknown",
|
||||||
|
})}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{scopes.includes("openid")
|
||||||
|
? t("authorizeSubtitle")
|
||||||
|
: t("authorizeSubtitleOAuth")}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
{scopes.includes("openid") && (
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
{scopes.map((id) => {
|
||||||
|
const scope = scopeMap.find((s) => s.id === id);
|
||||||
|
if (!scope) return null;
|
||||||
|
return (
|
||||||
|
<div key={scope.id} className="flex flex-row items-center gap-3">
|
||||||
|
<div className="p-2 flex flex-col items-center justify-center bg-card-foreground rounded-md">
|
||||||
|
{scope.icon}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<div className="text-md">{scope.name}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{scope.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CardContent>
|
||||||
|
)}
|
||||||
|
<CardFooter className="flex flex-col items-stretch gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={() => authorizeMutation.mutate()}
|
||||||
|
loading={authorizeMutation.isPending}
|
||||||
|
>
|
||||||
|
{t("authorizeTitle")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => navigate("/")}
|
||||||
|
disabled={authorizeMutation.isPending}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{t("cancelTitle")}
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,10 +8,10 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { useAppContext } from "@/context/app-context";
|
import { useAppContext } from "@/context/app-context";
|
||||||
import { useUserContext } from "@/context/user-context";
|
import { useUserContext } from "@/context/user-context";
|
||||||
import { isValidUrl } from "@/lib/utils";
|
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { Navigate, useLocation, useNavigate } from "react-router";
|
import { Navigate, useLocation, useNavigate } from "react-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { useRedirectUri } from "@/lib/hooks/redirect-uri";
|
||||||
|
|
||||||
export const ContinuePage = () => {
|
export const ContinuePage = () => {
|
||||||
const { cookieDomain, disableUiWarnings } = useAppContext();
|
const { cookieDomain, disableUiWarnings } = useAppContext();
|
||||||
@@ -20,48 +20,35 @@ export const ContinuePage = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [showRedirectButton, setShowRedirectButton] = useState(false);
|
const [showRedirectButton, setShowRedirectButton] = useState(false);
|
||||||
|
const hasRedirected = useRef(false);
|
||||||
|
|
||||||
const searchParams = new URLSearchParams(search);
|
const searchParams = new URLSearchParams(search);
|
||||||
const redirectUri = searchParams.get("redirect_uri");
|
const redirectUri = searchParams.get("redirect_uri");
|
||||||
|
|
||||||
const isValidRedirectUri =
|
const { url, valid, trusted, allowedProto, httpsDowngrade } = useRedirectUri(
|
||||||
redirectUri !== null ? isValidUrl(redirectUri) : false;
|
redirectUri,
|
||||||
const redirectUriObj = isValidRedirectUri
|
cookieDomain,
|
||||||
? new URL(redirectUri as string)
|
);
|
||||||
: null;
|
|
||||||
const isTrustedRedirectUri =
|
|
||||||
redirectUriObj !== null
|
|
||||||
? redirectUriObj.hostname === cookieDomain ||
|
|
||||||
redirectUriObj.hostname.endsWith(`.${cookieDomain}`)
|
|
||||||
: false;
|
|
||||||
const isAllowedRedirectProto =
|
|
||||||
redirectUriObj !== null
|
|
||||||
? redirectUriObj.protocol === "https:" ||
|
|
||||||
redirectUriObj.protocol === "http:"
|
|
||||||
: false;
|
|
||||||
const isHttpsDowngrade =
|
|
||||||
redirectUriObj !== null
|
|
||||||
? redirectUriObj.protocol === "http:" &&
|
|
||||||
window.location.protocol === "https:"
|
|
||||||
: false;
|
|
||||||
|
|
||||||
const handleRedirect = () => {
|
const handleRedirect = useCallback(() => {
|
||||||
setLoading(true);
|
hasRedirected.current = true;
|
||||||
window.location.assign(redirectUriObj!.toString());
|
setIsLoading(true);
|
||||||
};
|
window.location.assign(url!);
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasRedirected.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(!isValidRedirectUri ||
|
(!valid || !allowedProto || !trusted || httpsDowngrade) &&
|
||||||
!isAllowedRedirectProto ||
|
|
||||||
!isTrustedRedirectUri ||
|
|
||||||
isHttpsDowngrade) &&
|
|
||||||
!disableUiWarnings
|
!disableUiWarnings
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -72,7 +59,7 @@ export const ContinuePage = () => {
|
|||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
const reveal = setTimeout(() => {
|
const reveal = setTimeout(() => {
|
||||||
setLoading(false);
|
setIsLoading(false);
|
||||||
setShowRedirectButton(true);
|
setShowRedirectButton(true);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
@@ -80,22 +67,33 @@ export const ContinuePage = () => {
|
|||||||
clearTimeout(auto);
|
clearTimeout(auto);
|
||||||
clearTimeout(reveal);
|
clearTimeout(reveal);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [
|
||||||
|
isLoggedIn,
|
||||||
|
hasRedirected,
|
||||||
|
valid,
|
||||||
|
allowedProto,
|
||||||
|
trusted,
|
||||||
|
httpsDowngrade,
|
||||||
|
disableUiWarnings,
|
||||||
|
setIsLoading,
|
||||||
|
handleRedirect,
|
||||||
|
setShowRedirectButton,
|
||||||
|
]);
|
||||||
|
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
return (
|
return (
|
||||||
<Navigate
|
<Navigate
|
||||||
to={`/login?redirect_uri=${encodeURIComponent(redirectUri || "")}`}
|
to={`/login${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`}
|
||||||
replace
|
replace
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isValidRedirectUri || !isAllowedRedirectProto) {
|
if (!valid || !allowedProto) {
|
||||||
return <Navigate to="/logout" replace />;
|
return <Navigate to="/logout" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isTrustedRedirectUri && !disableUiWarnings) {
|
if (!trusted && !disableUiWarnings) {
|
||||||
return (
|
return (
|
||||||
<Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm">
|
<Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -115,8 +113,8 @@ export const ContinuePage = () => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardFooter className="flex flex-col items-stretch gap-2">
|
<CardFooter className="flex flex-col items-stretch gap-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleRedirect}
|
onClick={() => handleRedirect()}
|
||||||
loading={loading}
|
loading={isLoading}
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
>
|
>
|
||||||
{t("continueTitle")}
|
{t("continueTitle")}
|
||||||
@@ -124,7 +122,7 @@ export const ContinuePage = () => {
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => navigate("/logout")}
|
onClick={() => navigate("/logout")}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={loading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{t("cancelTitle")}
|
{t("cancelTitle")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -133,7 +131,7 @@ export const ContinuePage = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isHttpsDowngrade && !disableUiWarnings) {
|
if (httpsDowngrade && !disableUiWarnings) {
|
||||||
return (
|
return (
|
||||||
<Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm">
|
<Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -151,13 +149,17 @@ export const ContinuePage = () => {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardFooter className="flex flex-col items-stretch gap-2">
|
<CardFooter className="flex flex-col items-stretch gap-2">
|
||||||
<Button onClick={handleRedirect} loading={loading} variant="warning">
|
<Button
|
||||||
|
onClick={() => handleRedirect()}
|
||||||
|
loading={isLoading}
|
||||||
|
variant="warning"
|
||||||
|
>
|
||||||
{t("continueTitle")}
|
{t("continueTitle")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => navigate("/logout")}
|
onClick={() => navigate("/logout")}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={loading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{t("cancelTitle")}
|
{t("cancelTitle")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -176,7 +178,7 @@ export const ContinuePage = () => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
{showRedirectButton && (
|
{showRedirectButton && (
|
||||||
<CardFooter className="flex flex-col items-stretch">
|
<CardFooter className="flex flex-col items-stretch">
|
||||||
<Button onClick={handleRedirect}>
|
<Button onClick={() => handleRedirect()}>
|
||||||
{t("continueRedirectManually")}
|
{t("continueRedirectManually")}
|
||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
|
|||||||
@@ -5,15 +5,30 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useLocation } from "react-router";
|
||||||
|
|
||||||
export const ErrorPage = () => {
|
export const ErrorPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { search } = useLocation();
|
||||||
|
const searchParams = new URLSearchParams(search);
|
||||||
|
const error = searchParams.get("error") ?? "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="min-w-xs sm:min-w-sm">
|
<Card className="min-w-xs sm:min-w-sm">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-3xl">{t("errorTitle")}</CardTitle>
|
<CardTitle className="text-3xl">{t("errorTitle")}</CardTitle>
|
||||||
<CardDescription>{t("errorSubtitle")}</CardDescription>
|
<CardDescription className="flex flex-col gap-1.5">
|
||||||
|
{error ? (
|
||||||
|
<>
|
||||||
|
<p>{t("errorSubtitleInfo")}</p>
|
||||||
|
<pre>{error}</pre>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p>{t("errorSubtitle")}</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { OAuthButton } from "@/components/ui/oauth-button";
|
|||||||
import { SeperatorWithChildren } from "@/components/ui/separator";
|
import { SeperatorWithChildren } from "@/components/ui/separator";
|
||||||
import { useAppContext } from "@/context/app-context";
|
import { useAppContext } from "@/context/app-context";
|
||||||
import { useUserContext } from "@/context/user-context";
|
import { useUserContext } from "@/context/user-context";
|
||||||
|
import { useOIDCParams } from "@/lib/hooks/oidc";
|
||||||
import { LoginSchema } from "@/schemas/login-schema";
|
import { LoginSchema } from "@/schemas/login-schema";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import axios, { AxiosError } from "axios";
|
import axios, { AxiosError } from "axios";
|
||||||
@@ -39,26 +40,43 @@ export const LoginPage = () => {
|
|||||||
const { providers, title, oauthAutoRedirect } = useAppContext();
|
const { providers, title, oauthAutoRedirect } = useAppContext();
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [oauthAutoRedirectHandover, setOauthAutoRedirectHandover] =
|
|
||||||
useState(false);
|
|
||||||
const [showRedirectButton, setShowRedirectButton] = useState(false);
|
const [showRedirectButton, setShowRedirectButton] = useState(false);
|
||||||
|
|
||||||
|
const hasAutoRedirectedRef = useRef(false);
|
||||||
|
|
||||||
const redirectTimer = useRef<number | null>(null);
|
const redirectTimer = useRef<number | null>(null);
|
||||||
const redirectButtonTimer = useRef<number | null>(null);
|
const redirectButtonTimer = useRef<number | null>(null);
|
||||||
|
|
||||||
const searchParams = new URLSearchParams(search);
|
const searchParams = new URLSearchParams(search);
|
||||||
const redirectUri = searchParams.get("redirect_uri");
|
const {
|
||||||
|
values: props,
|
||||||
|
isOidc,
|
||||||
|
compiled: compiledOIDCParams,
|
||||||
|
} = useOIDCParams(searchParams);
|
||||||
|
|
||||||
|
const [isOauthAutoRedirect, setIsOauthAutoRedirect] = useState(
|
||||||
|
providers.find((provider) => provider.id === oauthAutoRedirect) !==
|
||||||
|
undefined && props.redirect_uri,
|
||||||
|
);
|
||||||
|
|
||||||
const oauthProviders = providers.filter(
|
const oauthProviders = providers.filter(
|
||||||
(provider) => provider.id !== "username",
|
(provider) => provider.id !== "local" && provider.id !== "ldap",
|
||||||
);
|
);
|
||||||
const userAuthConfigured =
|
const userAuthConfigured =
|
||||||
providers.find((provider) => provider.id === "username") !== undefined;
|
providers.find(
|
||||||
|
(provider) => provider.id === "local" || provider.id === "ldap",
|
||||||
|
) !== undefined;
|
||||||
|
|
||||||
const oauthMutation = useMutation({
|
const {
|
||||||
|
mutate: oauthMutate,
|
||||||
|
data: oauthData,
|
||||||
|
isPending: oauthIsPending,
|
||||||
|
variables: oauthVariables,
|
||||||
|
} = useMutation({
|
||||||
mutationFn: (provider: string) =>
|
mutationFn: (provider: string) =>
|
||||||
axios.get(
|
axios.get(
|
||||||
`/api/oauth/url/${provider}?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`,
|
`/api/oauth/url/${provider}${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`,
|
||||||
),
|
),
|
||||||
mutationKey: ["oauth"],
|
mutationKey: ["oauth"],
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
@@ -69,22 +87,28 @@ export const LoginPage = () => {
|
|||||||
redirectTimer.current = window.setTimeout(() => {
|
redirectTimer.current = window.setTimeout(() => {
|
||||||
window.location.replace(data.data.url);
|
window.location.replace(data.data.url);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
|
if (isOauthAutoRedirect) {
|
||||||
|
redirectButtonTimer.current = window.setTimeout(() => {
|
||||||
|
setShowRedirectButton(true);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
setOauthAutoRedirectHandover(false);
|
setIsOauthAutoRedirect(false);
|
||||||
toast.error(t("loginOauthFailTitle"), {
|
toast.error(t("loginOauthFailTitle"), {
|
||||||
description: t("loginOauthFailSubtitle"),
|
description: t("loginOauthFailSubtitle"),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const loginMutation = useMutation({
|
const { mutate: loginMutate, isPending: loginIsPending } = useMutation({
|
||||||
mutationFn: (values: LoginSchema) => axios.post("/api/user/login", values),
|
mutationFn: (values: LoginSchema) => axios.post("/api/user/login", values),
|
||||||
mutationKey: ["login"],
|
mutationKey: ["login"],
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
if (data.data.totpPending) {
|
if (data.data.totpPending) {
|
||||||
window.location.replace(
|
window.location.replace(
|
||||||
`/totp?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`,
|
`/totp${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -94,8 +118,12 @@ export const LoginPage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
redirectTimer.current = window.setTimeout(() => {
|
redirectTimer.current = window.setTimeout(() => {
|
||||||
|
if (isOidc) {
|
||||||
|
window.location.replace(`/authorize?${compiledOIDCParams}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
window.location.replace(
|
window.location.replace(
|
||||||
`/continue?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`,
|
`/continue${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`,
|
||||||
);
|
);
|
||||||
}, 500);
|
}, 500);
|
||||||
},
|
},
|
||||||
@@ -111,33 +139,43 @@ export const LoginPage = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
providers.find((provider) => provider.id === oauthAutoRedirect) &&
|
|
||||||
!isLoggedIn &&
|
!isLoggedIn &&
|
||||||
redirectUri
|
isOauthAutoRedirect &&
|
||||||
|
!hasAutoRedirectedRef.current &&
|
||||||
|
props.redirect_uri
|
||||||
) {
|
) {
|
||||||
// Not sure of a better way to do this
|
hasAutoRedirectedRef.current = true;
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
oauthMutate(oauthAutoRedirect);
|
||||||
setOauthAutoRedirectHandover(true);
|
|
||||||
oauthMutation.mutate(oauthAutoRedirect);
|
|
||||||
redirectButtonTimer.current = window.setTimeout(() => {
|
|
||||||
setShowRedirectButton(true);
|
|
||||||
}, 5000);
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, [
|
||||||
|
isLoggedIn,
|
||||||
|
oauthMutate,
|
||||||
|
hasAutoRedirectedRef,
|
||||||
|
oauthAutoRedirect,
|
||||||
|
isOauthAutoRedirect,
|
||||||
|
props.redirect_uri,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(
|
useEffect(() => {
|
||||||
() => () => {
|
return () => {
|
||||||
if (redirectTimer.current) clearTimeout(redirectTimer.current);
|
if (redirectTimer.current) {
|
||||||
if (redirectButtonTimer.current)
|
clearTimeout(redirectTimer.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redirectButtonTimer.current) {
|
||||||
clearTimeout(redirectButtonTimer.current);
|
clearTimeout(redirectButtonTimer.current);
|
||||||
},
|
}
|
||||||
[],
|
};
|
||||||
);
|
}, [redirectTimer, redirectButtonTimer]);
|
||||||
|
|
||||||
if (isLoggedIn && redirectUri) {
|
if (isLoggedIn && isOidc) {
|
||||||
|
return <Navigate to={`/authorize?${compiledOIDCParams}`} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoggedIn && props.redirect_uri !== "") {
|
||||||
return (
|
return (
|
||||||
<Navigate
|
<Navigate
|
||||||
to={`/continue?redirect_uri=${encodeURIComponent(redirectUri)}`}
|
to={`/continue${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`}
|
||||||
replace
|
replace
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -147,7 +185,7 @@ export const LoginPage = () => {
|
|||||||
return <Navigate to="/logout" replace />;
|
return <Navigate to="/logout" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oauthAutoRedirectHandover) {
|
if (isOauthAutoRedirect) {
|
||||||
return (
|
return (
|
||||||
<Card className="min-w-xs sm:min-w-sm">
|
<Card className="min-w-xs sm:min-w-sm">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -162,7 +200,14 @@ export const LoginPage = () => {
|
|||||||
<CardFooter className="flex flex-col items-stretch">
|
<CardFooter className="flex flex-col items-stretch">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.location.replace(oauthMutation.data?.data.url);
|
if (oauthData?.data.url) {
|
||||||
|
window.location.replace(oauthData.data.url);
|
||||||
|
} else {
|
||||||
|
setIsOauthAutoRedirect(false);
|
||||||
|
toast.error(t("loginOauthFailTitle"), {
|
||||||
|
description: t("loginOauthFailSubtitle"),
|
||||||
|
});
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("loginOauthAutoRedirectButton")}
|
{t("loginOauthAutoRedirectButton")}
|
||||||
@@ -193,12 +238,9 @@ export const LoginPage = () => {
|
|||||||
title={provider.name}
|
title={provider.name}
|
||||||
icon={iconMap[provider.id] ?? <OAuthIcon />}
|
icon={iconMap[provider.id] ?? <OAuthIcon />}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={() => oauthMutation.mutate(provider.id)}
|
onClick={() => oauthMutate(provider.id)}
|
||||||
loading={
|
loading={oauthIsPending && oauthVariables === provider.id}
|
||||||
oauthMutation.isPending &&
|
disabled={oauthIsPending || loginIsPending}
|
||||||
oauthMutation.variables === provider.id
|
|
||||||
}
|
|
||||||
disabled={oauthMutation.isPending || loginMutation.isPending}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -208,8 +250,8 @@ export const LoginPage = () => {
|
|||||||
)}
|
)}
|
||||||
{userAuthConfigured && (
|
{userAuthConfigured && (
|
||||||
<LoginForm
|
<LoginForm
|
||||||
onSubmit={(values) => loginMutation.mutate(values)}
|
onSubmit={(values) => loginMutate(values)}
|
||||||
loading={loginMutation.isPending || oauthMutation.isPending}
|
loading={loginIsPending || oauthIsPending}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{providers.length == 0 && (
|
{providers.length == 0 && (
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const LogoutPage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
redirectTimer.current = window.setTimeout(() => {
|
redirectTimer.current = window.setTimeout(() => {
|
||||||
window.location.assign("/login");
|
window.location.replace("/login");
|
||||||
}, 500);
|
}, 500);
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
@@ -39,12 +39,13 @@ export const LogoutPage = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(
|
useEffect(() => {
|
||||||
() => () => {
|
return () => {
|
||||||
if (redirectTimer.current) clearTimeout(redirectTimer.current);
|
if (redirectTimer.current) {
|
||||||
},
|
clearTimeout(redirectTimer.current);
|
||||||
[],
|
}
|
||||||
);
|
};
|
||||||
|
}, [redirectTimer]);
|
||||||
|
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
return <Navigate to="/login" replace />;
|
return <Navigate to="/login" replace />;
|
||||||
@@ -55,7 +56,7 @@ export const LogoutPage = () => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-3xl">{t("logoutTitle")}</CardTitle>
|
<CardTitle className="text-3xl">{t("logoutTitle")}</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{provider !== "username" ? (
|
{provider !== "local" && provider !== "ldap" ? (
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="logoutOauthSubtitle"
|
i18nKey="logoutOauthSubtitle"
|
||||||
t={t}
|
t={t}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { useEffect, useId, useRef } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Navigate, useLocation } from "react-router";
|
import { Navigate, useLocation } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { useOIDCParams } from "@/lib/hooks/oidc";
|
||||||
|
|
||||||
export const TotpPage = () => {
|
export const TotpPage = () => {
|
||||||
const { totpPending } = useUserContext();
|
const { totpPending } = useUserContext();
|
||||||
@@ -26,7 +27,11 @@ export const TotpPage = () => {
|
|||||||
const redirectTimer = useRef<number | null>(null);
|
const redirectTimer = useRef<number | null>(null);
|
||||||
|
|
||||||
const searchParams = new URLSearchParams(search);
|
const searchParams = new URLSearchParams(search);
|
||||||
const redirectUri = searchParams.get("redirect_uri");
|
const {
|
||||||
|
values: props,
|
||||||
|
isOidc,
|
||||||
|
compiled: compiledOIDCParams,
|
||||||
|
} = useOIDCParams(searchParams);
|
||||||
|
|
||||||
const totpMutation = useMutation({
|
const totpMutation = useMutation({
|
||||||
mutationFn: (values: TotpSchema) => axios.post("/api/user/totp", values),
|
mutationFn: (values: TotpSchema) => axios.post("/api/user/totp", values),
|
||||||
@@ -37,8 +42,13 @@ export const TotpPage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
redirectTimer.current = window.setTimeout(() => {
|
redirectTimer.current = window.setTimeout(() => {
|
||||||
|
if (isOidc) {
|
||||||
|
window.location.replace(`/authorize?${compiledOIDCParams}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
window.location.replace(
|
window.location.replace(
|
||||||
`/continue?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`,
|
`/continue${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`,
|
||||||
);
|
);
|
||||||
}, 500);
|
}, 500);
|
||||||
},
|
},
|
||||||
@@ -49,12 +59,13 @@ export const TotpPage = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(
|
useEffect(() => {
|
||||||
() => () => {
|
return () => {
|
||||||
if (redirectTimer.current) clearTimeout(redirectTimer.current);
|
if (redirectTimer.current) {
|
||||||
},
|
clearTimeout(redirectTimer.current);
|
||||||
[],
|
}
|
||||||
);
|
};
|
||||||
|
}, [redirectTimer]);
|
||||||
|
|
||||||
if (!totpPending) {
|
if (!totpPending) {
|
||||||
return <Navigate to="/" replace />;
|
return <Navigate to="/" replace />;
|
||||||
@@ -70,7 +81,6 @@ export const TotpPage = () => {
|
|||||||
<TotpForm
|
<TotpForm
|
||||||
formId={formId}
|
formId={formId}
|
||||||
onSubmit={(values) => totpMutation.mutate(values)}
|
onSubmit={(values) => totpMutation.mutate(values)}
|
||||||
loading={totpMutation.isPending}
|
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="flex flex-col items-stretch">
|
<CardFooter className="flex flex-col items-stretch">
|
||||||
|
|||||||
5
frontend/src/schemas/oidc-schemas.ts
Normal file
5
frontend/src/schemas/oidc-schemas.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const getOidcClientInfoSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
});
|
||||||
@@ -24,6 +24,11 @@ export default defineConfig({
|
|||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/resources/, ""),
|
rewrite: (path) => path.replace(/^\/resources/, ""),
|
||||||
},
|
},
|
||||||
|
"/.well-known": {
|
||||||
|
target: "http://tinyauth-backend:3000/.well-known",
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/\.well-known/, ""),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
allowedHosts: true,
|
allowedHosts: true,
|
||||||
},
|
},
|
||||||
|
|||||||
30
go.mod
30
go.mod
@@ -4,26 +4,28 @@ go 1.24.0
|
|||||||
|
|
||||||
toolchain go1.24.3
|
toolchain go1.24.3
|
||||||
|
|
||||||
|
replace github.com/traefik/paerser v0.2.2 => ./paerser
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3
|
github.com/cenkalti/backoff/v5 v5.0.3
|
||||||
github.com/charmbracelet/huh v0.8.0
|
github.com/charmbracelet/huh v0.8.0
|
||||||
github.com/docker/docker v28.5.2+incompatible
|
github.com/docker/docker v28.5.2+incompatible
|
||||||
github.com/gin-gonic/gin v1.11.0
|
github.com/gin-gonic/gin v1.11.0
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/go-jose/go-jose/v4 v4.1.3
|
||||||
github.com/go-ldap/ldap/v3 v3.4.12
|
github.com/go-ldap/ldap/v3 v3.4.12
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.1
|
github.com/golang-migrate/migrate/v4 v4.19.1
|
||||||
github.com/google/go-querystring v1.1.0
|
github.com/google/go-querystring v1.2.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/mdp/qrterminal/v3 v3.2.1
|
github.com/mdp/qrterminal/v3 v3.2.1
|
||||||
github.com/pquerna/otp v1.5.0
|
github.com/pquerna/otp v1.5.0
|
||||||
github.com/rs/zerolog v1.34.0
|
github.com/rs/zerolog v1.34.0
|
||||||
github.com/traefik/paerser v0.2.2
|
github.com/traefik/paerser v0.2.2
|
||||||
github.com/weppos/publicsuffix-go v0.50.1
|
github.com/weppos/publicsuffix-go v0.50.2
|
||||||
golang.org/x/crypto v0.46.0
|
golang.org/x/crypto v0.47.0
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
|
||||||
golang.org/x/oauth2 v0.34.0
|
golang.org/x/oauth2 v0.34.0
|
||||||
gorm.io/gorm v1.31.1
|
|
||||||
gotest.tools/v3 v3.5.2
|
gotest.tools/v3 v3.5.2
|
||||||
|
modernc.org/sqlite v1.44.3
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -59,7 +61,6 @@ require (
|
|||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
@@ -71,8 +72,6 @@ require (
|
|||||||
github.com/google/go-cmp v0.7.0 // indirect
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
github.com/huandu/xstrings v1.5.0 // indirect
|
github.com/huandu/xstrings v1.5.0 // indirect
|
||||||
github.com/imdario/mergo v0.3.11 // indirect
|
github.com/imdario/mergo v0.3.11 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
@@ -93,7 +92,7 @@ require (
|
|||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
@@ -114,16 +113,15 @@ require (
|
|||||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||||
golang.org/x/arch v0.20.0 // indirect
|
golang.org/x/arch v0.20.0 // indirect
|
||||||
golang.org/x/net v0.47.0 // indirect
|
golang.org/x/net v0.48.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.39.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/term v0.38.0 // indirect
|
golang.org/x/term v0.39.0 // indirect
|
||||||
golang.org/x/text v0.32.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.9 // indirect
|
google.golang.org/protobuf v1.36.9 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.66.3 // indirect
|
modernc.org/libc v1.67.6 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
modernc.org/sqlite v1.38.2 // indirect
|
|
||||||
rsc.io/qr v0.2.0 // indirect
|
rsc.io/qr v0.2.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
85
go.sum
85
go.sum
@@ -101,12 +101,10 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
|
|||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
|
||||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
|
||||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
|
||||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
|
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
|
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
@@ -129,11 +127,11 @@ github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7Lk
|
|||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
|
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
|
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
||||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
@@ -144,6 +142,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3Ar
|
|||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
|
||||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
@@ -161,10 +161,6 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
|
|||||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
@@ -223,8 +219,8 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
|
|||||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||||
@@ -271,14 +267,12 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
|||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/traefik/paerser v0.2.2 h1:cpzW/ZrQrBh3mdwD/jnp6aXASiUFKOVr6ldP+keJTcQ=
|
|
||||||
github.com/traefik/paerser v0.2.2/go.mod h1:7BBDd4FANoVgaTZG+yh26jI6CA2nds7D/4VTEdIsh24=
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
github.com/weppos/publicsuffix-go v0.50.1 h1:elrBHeSkS/eIb169+DnLrknqmdP4AjT0Q0tEdytz1Og=
|
github.com/weppos/publicsuffix-go v0.50.2 h1:KsJFc8IEKTJovM46SRCnGNsM+rFShxcs6VEHjOJcXzE=
|
||||||
github.com/weppos/publicsuffix-go v0.50.1/go.mod h1:znn0JVXjcR5hpUl9pbEogwH6I710rA1AX0QQPT0bf+k=
|
github.com/weppos/publicsuffix-go v0.50.2/go.mod h1:CbQCKDtXF8UcT7hrxeMa0MDjwhpOI9iYOU7cfq+yo8k=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
@@ -309,19 +303,19 @@ golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
|||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -338,28 +332,27 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c h1:AtEkQdl5b6zsybXcbz00j1LwNodDuH6hVifIaNqk7NQ=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c h1:AtEkQdl5b6zsybXcbz00j1LwNodDuH6hVifIaNqk7NQ=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA=
|
||||||
@@ -378,22 +371,22 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
|
||||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
|
||||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||||
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
|
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||||
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
||||||
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
|
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||||
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
|
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
||||||
|
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
|
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
|
||||||
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
|
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
@@ -402,8 +395,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
|||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY=
|
||||||
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
|||||||
1
internal/assets/migrations/000004_created_at.down.sql
Normal file
1
internal/assets/migrations/000004_created_at.down.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "sessions" DROP COLUMN "created_at";
|
||||||
1
internal/assets/migrations/000004_created_at.up.sql
Normal file
1
internal/assets/migrations/000004_created_at.up.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "sessions" ADD COLUMN "created_at" INTEGER NOT NULL DEFAULT 0;
|
||||||
3
internal/assets/migrations/000005_oidc_session.down.sql
Normal file
3
internal/assets/migrations/000005_oidc_session.down.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
DROP TABLE IF EXISTS "oidc_tokens";
|
||||||
|
DROP TABLE IF EXISTS "oidc_userinfo";
|
||||||
|
DROP TABLE IF EXISTS "oidc_codes";
|
||||||
27
internal/assets/migrations/000005_oidc_session.up.sql
Normal file
27
internal/assets/migrations/000005_oidc_session.up.sql
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "oidc_codes" (
|
||||||
|
"sub" TEXT NOT NULL UNIQUE,
|
||||||
|
"code_hash" TEXT NOT NULL PRIMARY KEY UNIQUE,
|
||||||
|
"scope" TEXT NOT NULL,
|
||||||
|
"redirect_uri" TEXT NOT NULL,
|
||||||
|
"client_id" TEXT NOT NULL,
|
||||||
|
"expires_at" INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "oidc_tokens" (
|
||||||
|
"sub" TEXT NOT NULL UNIQUE,
|
||||||
|
"access_token_hash" TEXT NOT NULL PRIMARY KEY UNIQUE,
|
||||||
|
"refresh_token_hash" TEXT NOT NULL,
|
||||||
|
"scope" TEXT NOT NULL,
|
||||||
|
"client_id" TEXT NOT NULL,
|
||||||
|
"token_expires_at" INTEGER NOT NULL,
|
||||||
|
"refresh_token_expires_at" INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "oidc_userinfo" (
|
||||||
|
"sub" TEXT NOT NULL UNIQUE PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"preferred_username" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"groups" TEXT NOT NULL,
|
||||||
|
"updated_at" INTEGER NOT NULL
|
||||||
|
);
|
||||||
@@ -14,16 +14,15 @@ import (
|
|||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||||
"github.com/steveiliop56/tinyauth/internal/model"
|
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type BootstrapApp struct {
|
type BootstrapApp struct {
|
||||||
config config.Config
|
config config.Config
|
||||||
context struct {
|
context struct {
|
||||||
|
appUrl string
|
||||||
uuid string
|
uuid string
|
||||||
cookieDomain string
|
cookieDomain string
|
||||||
sessionCookieName string
|
sessionCookieName string
|
||||||
@@ -32,6 +31,7 @@ type BootstrapApp struct {
|
|||||||
users []config.User
|
users []config.User
|
||||||
oauthProviders map[string]config.OAuthServiceConfig
|
oauthProviders map[string]config.OAuthServiceConfig
|
||||||
configuredProviders []controller.Provider
|
configuredProviders []controller.Provider
|
||||||
|
oidcClients []config.OIDCClientConfig
|
||||||
}
|
}
|
||||||
services Services
|
services Services
|
||||||
}
|
}
|
||||||
@@ -43,6 +43,20 @@ func NewBootstrapApp(config config.Config) *BootstrapApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *BootstrapApp) Setup() error {
|
func (app *BootstrapApp) Setup() error {
|
||||||
|
// get app url
|
||||||
|
appUrl, err := url.Parse(app.config.AppURL)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
app.context.appUrl = appUrl.Scheme + "://" + appUrl.Host
|
||||||
|
|
||||||
|
// validate session config
|
||||||
|
if app.config.Auth.SessionMaxLifetime != 0 && app.config.Auth.SessionMaxLifetime < app.config.Auth.SessionExpiry {
|
||||||
|
return fmt.Errorf("session max lifetime cannot be less than session expiry")
|
||||||
|
}
|
||||||
|
|
||||||
// Parse users
|
// Parse users
|
||||||
users, err := utils.GetUsers(app.config.Auth.Users, app.config.Auth.UsersFile)
|
users, err := utils.GetUsers(app.config.Auth.Users, app.config.Auth.UsersFile)
|
||||||
|
|
||||||
@@ -59,16 +73,12 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
secret := utils.GetSecret(provider.ClientSecret, provider.ClientSecretFile)
|
secret := utils.GetSecret(provider.ClientSecret, provider.ClientSecretFile)
|
||||||
provider.ClientSecret = secret
|
provider.ClientSecret = secret
|
||||||
provider.ClientSecretFile = ""
|
provider.ClientSecretFile = ""
|
||||||
app.context.oauthProviders[name] = provider
|
|
||||||
}
|
|
||||||
|
|
||||||
for id := range config.OverrideProviders {
|
if provider.RedirectURL == "" {
|
||||||
if provider, exists := app.context.oauthProviders[id]; exists {
|
provider.RedirectURL = app.context.appUrl + "/api/oauth/callback/" + name
|
||||||
if provider.RedirectURL == "" {
|
|
||||||
provider.RedirectURL = app.config.AppURL + "/api/oauth/callback/" + id
|
|
||||||
app.context.oauthProviders[id] = provider
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.context.oauthProviders[name] = provider
|
||||||
}
|
}
|
||||||
|
|
||||||
for id, provider := range app.context.oauthProviders {
|
for id, provider := range app.context.oauthProviders {
|
||||||
@@ -82,8 +92,14 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
app.context.oauthProviders[id] = provider
|
app.context.oauthProviders[id] = provider
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup OIDC clients
|
||||||
|
for id, client := range app.config.OIDC.Clients {
|
||||||
|
client.ID = id
|
||||||
|
app.context.oidcClients = append(app.context.oidcClients, client)
|
||||||
|
}
|
||||||
|
|
||||||
// Get cookie domain
|
// Get cookie domain
|
||||||
cookieDomain, err := utils.GetCookieDomain(app.config.AppURL)
|
cookieDomain, err := utils.GetCookieDomain(app.context.appUrl)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -92,7 +108,6 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
app.context.cookieDomain = cookieDomain
|
app.context.cookieDomain = cookieDomain
|
||||||
|
|
||||||
// Cookie names
|
// Cookie names
|
||||||
appUrl, _ := url.Parse(app.config.AppURL) // Already validated
|
|
||||||
app.context.uuid = utils.GenerateUUID(appUrl.Hostname())
|
app.context.uuid = utils.GenerateUUID(appUrl.Hostname())
|
||||||
cookieId := strings.Split(app.context.uuid, "-")[0]
|
cookieId := strings.Split(app.context.uuid, "-")[0]
|
||||||
app.context.sessionCookieName = fmt.Sprintf("%s-%s", config.SessionCookieName, cookieId)
|
app.context.sessionCookieName = fmt.Sprintf("%s-%s", config.SessionCookieName, cookieId)
|
||||||
@@ -100,16 +115,26 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
app.context.redirectCookieName = fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId)
|
app.context.redirectCookieName = fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId)
|
||||||
|
|
||||||
// Dumps
|
// Dumps
|
||||||
log.Trace().Interface("config", app.config).Msg("Config dump")
|
tlog.App.Trace().Interface("config", app.config).Msg("Config dump")
|
||||||
log.Trace().Interface("users", app.context.users).Msg("Users dump")
|
tlog.App.Trace().Interface("users", app.context.users).Msg("Users dump")
|
||||||
log.Trace().Interface("oauthProviders", app.context.oauthProviders).Msg("OAuth providers dump")
|
tlog.App.Trace().Interface("oauthProviders", app.context.oauthProviders).Msg("OAuth providers dump")
|
||||||
log.Trace().Str("cookieDomain", app.context.cookieDomain).Msg("Cookie domain")
|
tlog.App.Trace().Str("cookieDomain", app.context.cookieDomain).Msg("Cookie domain")
|
||||||
log.Trace().Str("sessionCookieName", app.context.sessionCookieName).Msg("Session cookie name")
|
tlog.App.Trace().Str("sessionCookieName", app.context.sessionCookieName).Msg("Session cookie name")
|
||||||
log.Trace().Str("csrfCookieName", app.context.csrfCookieName).Msg("CSRF cookie name")
|
tlog.App.Trace().Str("csrfCookieName", app.context.csrfCookieName).Msg("CSRF cookie name")
|
||||||
log.Trace().Str("redirectCookieName", app.context.redirectCookieName).Msg("Redirect cookie name")
|
tlog.App.Trace().Str("redirectCookieName", app.context.redirectCookieName).Msg("Redirect cookie name")
|
||||||
|
|
||||||
|
// Database
|
||||||
|
db, err := app.SetupDatabase(app.config.DatabasePath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to setup database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queries
|
||||||
|
queries := repository.New(db)
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
services, err := app.initServices()
|
services, err := app.initServices(queries)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to initialize services: %w", err)
|
return fmt.Errorf("failed to initialize services: %w", err)
|
||||||
@@ -132,15 +157,23 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
return configuredProviders[i].Name < configuredProviders[j].Name
|
return configuredProviders[i].Name < configuredProviders[j].Name
|
||||||
})
|
})
|
||||||
|
|
||||||
if services.authService.UserAuthConfigured() {
|
if services.authService.LocalAuthConfigured() {
|
||||||
configuredProviders = append(configuredProviders, controller.Provider{
|
configuredProviders = append(configuredProviders, controller.Provider{
|
||||||
Name: "Username",
|
Name: "Local",
|
||||||
ID: "username",
|
ID: "local",
|
||||||
OAuth: false,
|
OAuth: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Interface("providers", configuredProviders).Msg("Authentication providers")
|
if services.authService.LdapAuthConfigured() {
|
||||||
|
configuredProviders = append(configuredProviders, controller.Provider{
|
||||||
|
Name: "LDAP",
|
||||||
|
ID: "ldap",
|
||||||
|
OAuth: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
tlog.App.Debug().Interface("providers", configuredProviders).Msg("Authentication providers")
|
||||||
|
|
||||||
if len(configuredProviders) == 0 {
|
if len(configuredProviders) == 0 {
|
||||||
return fmt.Errorf("no authentication providers configured")
|
return fmt.Errorf("no authentication providers configured")
|
||||||
@@ -155,29 +188,29 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
return fmt.Errorf("failed to setup routes: %w", err)
|
return fmt.Errorf("failed to setup routes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start DB cleanup routine
|
// Start db cleanup routine
|
||||||
log.Debug().Msg("Starting database cleanup routine")
|
tlog.App.Debug().Msg("Starting database cleanup routine")
|
||||||
go app.dbCleanup(services.databaseService.GetDatabase())
|
go app.dbCleanup(queries)
|
||||||
|
|
||||||
// If analytics are not disabled, start heartbeat
|
// If analytics are not disabled, start heartbeat
|
||||||
if !app.config.DisableAnalytics {
|
if !app.config.DisableAnalytics {
|
||||||
log.Debug().Msg("Starting heartbeat routine")
|
tlog.App.Debug().Msg("Starting heartbeat routine")
|
||||||
go app.heartbeat()
|
go app.heartbeat()
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have an socket path, bind to it
|
// If we have an socket path, bind to it
|
||||||
if app.config.Server.SocketPath != "" {
|
if app.config.Server.SocketPath != "" {
|
||||||
if _, err := os.Stat(app.config.Server.SocketPath); err == nil {
|
if _, err := os.Stat(app.config.Server.SocketPath); err == nil {
|
||||||
log.Info().Msgf("Removing existing socket file %s", app.config.Server.SocketPath)
|
tlog.App.Info().Msgf("Removing existing socket file %s", app.config.Server.SocketPath)
|
||||||
err := os.Remove(app.config.Server.SocketPath)
|
err := os.Remove(app.config.Server.SocketPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to remove existing socket file: %w", err)
|
return fmt.Errorf("failed to remove existing socket file: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Msgf("Starting server on unix socket %s", app.config.Server.SocketPath)
|
tlog.App.Info().Msgf("Starting server on unix socket %s", app.config.Server.SocketPath)
|
||||||
if err := router.RunUnix(app.config.Server.SocketPath); err != nil {
|
if err := router.RunUnix(app.config.Server.SocketPath); err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to start server")
|
tlog.App.Fatal().Err(err).Msg("Failed to start server")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -185,9 +218,9 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
address := fmt.Sprintf("%s:%d", app.config.Server.Address, app.config.Server.Port)
|
address := fmt.Sprintf("%s:%d", app.config.Server.Address, app.config.Server.Port)
|
||||||
log.Info().Msgf("Starting server on %s", address)
|
tlog.App.Info().Msgf("Starting server on %s", address)
|
||||||
if err := router.Run(address); err != nil {
|
if err := router.Run(address); err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to start server")
|
tlog.App.Fatal().Err(err).Msg("Failed to start server")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -210,7 +243,7 @@ func (app *BootstrapApp) heartbeat() {
|
|||||||
bodyJson, err := json.Marshal(body)
|
bodyJson, err := json.Marshal(body)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to marshal heartbeat body")
|
tlog.App.Error().Err(err).Msg("Failed to marshal heartbeat body")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,13 +253,13 @@ func (app *BootstrapApp) heartbeat() {
|
|||||||
|
|
||||||
heartbeatURL := config.ApiServer + "/v1/instances/heartbeat"
|
heartbeatURL := config.ApiServer + "/v1/instances/heartbeat"
|
||||||
|
|
||||||
for ; true; <-ticker.C {
|
for range ticker.C {
|
||||||
log.Debug().Msg("Sending heartbeat")
|
tlog.App.Debug().Msg("Sending heartbeat")
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodPost, heartbeatURL, bytes.NewReader(bodyJson))
|
req, err := http.NewRequest(http.MethodPost, heartbeatURL, bytes.NewReader(bodyJson))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to create heartbeat request")
|
tlog.App.Error().Err(err).Msg("Failed to create heartbeat request")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,28 +268,28 @@ func (app *BootstrapApp) heartbeat() {
|
|||||||
res, err := client.Do(req)
|
res, err := client.Do(req)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to send heartbeat")
|
tlog.App.Error().Err(err).Msg("Failed to send heartbeat")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
res.Body.Close()
|
res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != 200 && res.StatusCode != 201 {
|
if res.StatusCode != 200 && res.StatusCode != 201 {
|
||||||
log.Debug().Str("status", res.Status).Msg("Heartbeat returned non-200/201 status")
|
tlog.App.Debug().Str("status", res.Status).Msg("Heartbeat returned non-200/201 status")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *BootstrapApp) dbCleanup(db *gorm.DB) {
|
func (app *BootstrapApp) dbCleanup(queries *repository.Queries) {
|
||||||
ticker := time.NewTicker(time.Duration(30) * time.Minute)
|
ticker := time.NewTicker(time.Duration(30) * time.Minute)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
for ; true; <-ticker.C {
|
for range ticker.C {
|
||||||
log.Debug().Msg("Cleaning up old database sessions")
|
tlog.App.Debug().Msg("Cleaning up old database sessions")
|
||||||
_, err := gorm.G[model.Session](db).Where("expiry < ?", time.Now().Unix()).Delete(ctx)
|
err := queries.DeleteExpiredSessions(ctx, time.Now().Unix())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to cleanup old sessions")
|
tlog.App.Error().Err(err).Msg("Failed to clean up old database sessions")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
57
internal/bootstrap/db_bootstrap.go
Normal file
57
internal/bootstrap/db_bootstrap.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/assets"
|
||||||
|
|
||||||
|
"github.com/golang-migrate/migrate/v4"
|
||||||
|
"github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||||
|
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *BootstrapApp) SetupDatabase(databasePath string) (*sql.DB, error) {
|
||||||
|
dir := filepath.Dir(databasePath)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(dir, 0750); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create database directory %s: %w", dir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := sql.Open("sqlite", databasePath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit to 1 connection to sequence writes, this may need to be revisited in the future
|
||||||
|
// if the sqlite connection starts being a bottleneck
|
||||||
|
db.SetMaxOpenConns(1)
|
||||||
|
|
||||||
|
migrations, err := iofs.New(assets.Migrations, "migrations")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create migrations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
target, err := sqlite3.WithInstance(db, &sqlite3.Config{})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create sqlite3 instance: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
migrator, err := migrate.NewWithInstance("iofs", migrations, "sqlite3", target)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create migrator: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := migrator.Up(); err != nil && err != migrate.ErrNoChange {
|
||||||
|
return nil, fmt.Errorf("failed to migrate database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
@@ -2,20 +2,27 @@ package bootstrap
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"slices"
|
||||||
|
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||||
"github.com/steveiliop56/tinyauth/internal/middleware"
|
"github.com/steveiliop56/tinyauth/internal/middleware"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var DEV_MODES = []string{"main", "test", "development"}
|
||||||
|
|
||||||
func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
||||||
|
if !slices.Contains(DEV_MODES, config.Version) {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
}
|
||||||
|
|
||||||
engine := gin.New()
|
engine := gin.New()
|
||||||
engine.Use(gin.Recovery())
|
engine.Use(gin.Recovery())
|
||||||
|
|
||||||
if len(app.config.Server.TrustedProxies) > 0 {
|
if len(app.config.Server.TrustedProxies) > 0 {
|
||||||
err := engine.SetTrustedProxies(strings.Split(app.config.Server.TrustedProxies, ","))
|
err := engine.SetTrustedProxies(app.config.Server.TrustedProxies)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to set trusted proxies: %w", err)
|
return nil, fmt.Errorf("failed to set trusted proxies: %w", err)
|
||||||
@@ -79,6 +86,10 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
|||||||
|
|
||||||
oauthController.SetupRoutes()
|
oauthController.SetupRoutes()
|
||||||
|
|
||||||
|
oidcController := controller.NewOIDCController(controller.OIDCControllerConfig{}, app.services.oidcService, apiRouter)
|
||||||
|
|
||||||
|
oidcController.SetupRoutes()
|
||||||
|
|
||||||
proxyController := controller.NewProxyController(controller.ProxyControllerConfig{
|
proxyController := controller.NewProxyController(controller.ProxyControllerConfig{
|
||||||
AppURL: app.config.AppURL,
|
AppURL: app.config.AppURL,
|
||||||
}, apiRouter, app.services.accessControlService, app.services.authService)
|
}, apiRouter, app.services.accessControlService, app.services.authService)
|
||||||
@@ -102,5 +113,9 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
|||||||
|
|
||||||
healthController.SetupRoutes()
|
healthController.SetupRoutes()
|
||||||
|
|
||||||
|
wellknownController := controller.NewWellKnownController(controller.WellKnownControllerConfig{}, app.services.oidcService, engine)
|
||||||
|
|
||||||
|
wellknownController.SetupRoutes()
|
||||||
|
|
||||||
return engine, nil
|
return engine, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,23 @@
|
|||||||
package bootstrap
|
package bootstrap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Services struct {
|
type Services struct {
|
||||||
accessControlService *service.AccessControlsService
|
accessControlService *service.AccessControlsService
|
||||||
authService *service.AuthService
|
authService *service.AuthService
|
||||||
databaseService *service.DatabaseService
|
|
||||||
dockerService *service.DockerService
|
dockerService *service.DockerService
|
||||||
ldapService *service.LdapService
|
ldapService *service.LdapService
|
||||||
oauthBrokerService *service.OAuthBrokerService
|
oauthBrokerService *service.OAuthBrokerService
|
||||||
|
oidcService *service.OIDCService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *BootstrapApp) initServices() (Services, error) {
|
func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, error) {
|
||||||
services := Services{}
|
services := Services{}
|
||||||
|
|
||||||
databaseService := service.NewDatabaseService(service.DatabaseServiceConfig{
|
|
||||||
DatabasePath: app.config.DatabasePath,
|
|
||||||
})
|
|
||||||
|
|
||||||
err := databaseService.Init()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return Services{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
services.databaseService = databaseService
|
|
||||||
|
|
||||||
ldapService := service.NewLdapService(service.LdapServiceConfig{
|
ldapService := service.NewLdapService(service.LdapServiceConfig{
|
||||||
Address: app.config.Ldap.Address,
|
Address: app.config.Ldap.Address,
|
||||||
BindDN: app.config.Ldap.BindDN,
|
BindDN: app.config.Ldap.BindDN,
|
||||||
@@ -37,16 +25,19 @@ func (app *BootstrapApp) initServices() (Services, error) {
|
|||||||
BaseDN: app.config.Ldap.BaseDN,
|
BaseDN: app.config.Ldap.BaseDN,
|
||||||
Insecure: app.config.Ldap.Insecure,
|
Insecure: app.config.Ldap.Insecure,
|
||||||
SearchFilter: app.config.Ldap.SearchFilter,
|
SearchFilter: app.config.Ldap.SearchFilter,
|
||||||
|
AuthCert: app.config.Ldap.AuthCert,
|
||||||
|
AuthKey: app.config.Ldap.AuthKey,
|
||||||
})
|
})
|
||||||
|
|
||||||
err = ldapService.Init()
|
err := ldapService.Init()
|
||||||
|
|
||||||
if err == nil {
|
if err != nil {
|
||||||
services.ldapService = ldapService
|
tlog.App.Warn().Err(err).Msg("Failed to setup LDAP service, starting without it")
|
||||||
} else {
|
ldapService.Unconfigure()
|
||||||
log.Warn().Err(err).Msg("Failed to initialize LDAP service, continuing without it")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
services.ldapService = ldapService
|
||||||
|
|
||||||
dockerService := service.NewDockerService()
|
dockerService := service.NewDockerService()
|
||||||
|
|
||||||
err = dockerService.Init()
|
err = dockerService.Init()
|
||||||
@@ -57,7 +48,7 @@ func (app *BootstrapApp) initServices() (Services, error) {
|
|||||||
|
|
||||||
services.dockerService = dockerService
|
services.dockerService = dockerService
|
||||||
|
|
||||||
accessControlsService := service.NewAccessControlsService(dockerService)
|
accessControlsService := service.NewAccessControlsService(dockerService, app.config.Apps)
|
||||||
|
|
||||||
err = accessControlsService.Init()
|
err = accessControlsService.Init()
|
||||||
|
|
||||||
@@ -68,15 +59,18 @@ func (app *BootstrapApp) initServices() (Services, error) {
|
|||||||
services.accessControlService = accessControlsService
|
services.accessControlService = accessControlsService
|
||||||
|
|
||||||
authService := service.NewAuthService(service.AuthServiceConfig{
|
authService := service.NewAuthService(service.AuthServiceConfig{
|
||||||
Users: app.context.users,
|
Users: app.context.users,
|
||||||
OauthWhitelist: app.config.OAuth.Whitelist,
|
OauthWhitelist: app.config.OAuth.Whitelist,
|
||||||
SessionExpiry: app.config.Auth.SessionExpiry,
|
SessionExpiry: app.config.Auth.SessionExpiry,
|
||||||
SecureCookie: app.config.Auth.SecureCookie,
|
SessionMaxLifetime: app.config.Auth.SessionMaxLifetime,
|
||||||
CookieDomain: app.context.cookieDomain,
|
SecureCookie: app.config.Auth.SecureCookie,
|
||||||
LoginTimeout: app.config.Auth.LoginTimeout,
|
CookieDomain: app.context.cookieDomain,
|
||||||
LoginMaxRetries: app.config.Auth.LoginMaxRetries,
|
LoginTimeout: app.config.Auth.LoginTimeout,
|
||||||
SessionCookieName: app.context.sessionCookieName,
|
LoginMaxRetries: app.config.Auth.LoginMaxRetries,
|
||||||
}, dockerService, ldapService, databaseService.GetDatabase())
|
SessionCookieName: app.context.sessionCookieName,
|
||||||
|
IP: app.config.Auth.IP,
|
||||||
|
LDAPGroupsCacheTTL: app.config.Ldap.GroupCacheTTL,
|
||||||
|
}, dockerService, services.ldapService, queries)
|
||||||
|
|
||||||
err = authService.Init()
|
err = authService.Init()
|
||||||
|
|
||||||
@@ -96,5 +90,21 @@ func (app *BootstrapApp) initServices() (Services, error) {
|
|||||||
|
|
||||||
services.oauthBrokerService = oauthBrokerService
|
services.oauthBrokerService = oauthBrokerService
|
||||||
|
|
||||||
|
oidcService := service.NewOIDCService(service.OIDCServiceConfig{
|
||||||
|
Clients: app.config.OIDC.Clients,
|
||||||
|
PrivateKeyPath: app.config.OIDC.PrivateKeyPath,
|
||||||
|
PublicKeyPath: app.config.OIDC.PublicKeyPath,
|
||||||
|
Issuer: app.config.AppURL,
|
||||||
|
SessionExpiry: app.config.Auth.SessionExpiry,
|
||||||
|
}, queries)
|
||||||
|
|
||||||
|
err = oidcService.Init()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return Services{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
services.oidcService = oidcService
|
||||||
|
|
||||||
return services, nil
|
return services, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,43 +16,57 @@ var RedirectCookieName = "tinyauth-redirect"
|
|||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl"`
|
AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl"`
|
||||||
LogLevel string `description:"Log level (trace, debug, info, warn, error)." yaml:"logLevel"`
|
|
||||||
ResourcesDir string `description:"The directory where resources are stored." yaml:"resourcesDir"`
|
ResourcesDir string `description:"The directory where resources are stored." yaml:"resourcesDir"`
|
||||||
DatabasePath string `description:"The path to the database file." yaml:"databasePath"`
|
DatabasePath string `description:"The path to the database file." yaml:"databasePath"`
|
||||||
DisableAnalytics bool `description:"Disable analytics." yaml:"disableAnalytics"`
|
DisableAnalytics bool `description:"Disable analytics." yaml:"disableAnalytics"`
|
||||||
DisableResources bool `description:"Disable resources server." yaml:"disableResources"`
|
DisableResources bool `description:"Disable resources server." yaml:"disableResources"`
|
||||||
DisableUIWarnings bool `description:"Disable UI warnings." yaml:"disableUIWarnings"`
|
DisableUIWarnings bool `description:"Disable UI warnings." yaml:"disableUIWarnings"`
|
||||||
LogJSON bool `description:"Enable JSON formatted logs." yaml:"logJSON"`
|
|
||||||
Server ServerConfig `description:"Server configuration." yaml:"server"`
|
Server ServerConfig `description:"Server configuration." yaml:"server"`
|
||||||
Auth AuthConfig `description:"Authentication configuration." yaml:"auth"`
|
Auth AuthConfig `description:"Authentication configuration." yaml:"auth"`
|
||||||
|
Apps map[string]App `description:"Application ACLs configuration." yaml:"apps"`
|
||||||
OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth"`
|
OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth"`
|
||||||
|
OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc"`
|
||||||
UI UIConfig `description:"UI customization." yaml:"ui"`
|
UI UIConfig `description:"UI customization." yaml:"ui"`
|
||||||
Ldap LdapConfig `description:"LDAP configuration." yaml:"ldap"`
|
Ldap LdapConfig `description:"LDAP configuration." yaml:"ldap"`
|
||||||
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
|
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
|
||||||
|
Log LogConfig `description:"Logging configuration." yaml:"log"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
Port int `description:"The port on which the server listens." yaml:"port"`
|
Port int `description:"The port on which the server listens." yaml:"port"`
|
||||||
Address string `description:"The address on which the server listens." yaml:"address"`
|
Address string `description:"The address on which the server listens." yaml:"address"`
|
||||||
SocketPath string `description:"The path to the Unix socket." yaml:"socketPath"`
|
SocketPath string `description:"The path to the Unix socket." yaml:"socketPath"`
|
||||||
TrustedProxies string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
|
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthConfig struct {
|
type AuthConfig struct {
|
||||||
Users string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"`
|
IP IPConfig `description:"IP whitelisting config options." yaml:"ip"`
|
||||||
UsersFile string `description:"Path to the users file." yaml:"usersFile"`
|
Users []string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"`
|
||||||
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"`
|
UsersFile string `description:"Path to the users file." yaml:"usersFile"`
|
||||||
SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry"`
|
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"`
|
||||||
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"`
|
SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry"`
|
||||||
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"`
|
SessionMaxLifetime int `description:"Maximum session lifetime in seconds." yaml:"sessionMaxLifetime"`
|
||||||
|
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"`
|
||||||
|
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IPConfig struct {
|
||||||
|
Allow []string `description:"List of allowed IPs or CIDR ranges." yaml:"allow"`
|
||||||
|
Block []string `description:"List of blocked IPs or CIDR ranges." yaml:"block"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OAuthConfig struct {
|
type OAuthConfig struct {
|
||||||
Whitelist string `description:"Comma-separated list of allowed OAuth domains." yaml:"whitelist"`
|
Whitelist []string `description:"Comma-separated list of allowed OAuth domains." yaml:"whitelist"`
|
||||||
AutoRedirect string `description:"The OAuth provider to use for automatic redirection." yaml:"autoRedirect"`
|
AutoRedirect string `description:"The OAuth provider to use for automatic redirection." yaml:"autoRedirect"`
|
||||||
Providers map[string]OAuthServiceConfig `description:"OAuth providers configuration." yaml:"providers"`
|
Providers map[string]OAuthServiceConfig `description:"OAuth providers configuration." yaml:"providers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OIDCConfig struct {
|
||||||
|
PrivateKeyPath string `description:"Path to the private key file." yaml:"privateKeyPath"`
|
||||||
|
PublicKeyPath string `description:"Path to the public key file." yaml:"publicKeyPath"`
|
||||||
|
Clients map[string]OIDCClientConfig `description:"OIDC clients configuration." yaml:"clients"`
|
||||||
|
}
|
||||||
|
|
||||||
type UIConfig struct {
|
type UIConfig struct {
|
||||||
Title string `description:"The title of the UI." yaml:"title"`
|
Title string `description:"The title of the UI." yaml:"title"`
|
||||||
ForgotPasswordMessage string `description:"Message displayed on the forgot password page." yaml:"forgotPasswordMessage"`
|
ForgotPasswordMessage string `description:"Message displayed on the forgot password page." yaml:"forgotPasswordMessage"`
|
||||||
@@ -60,12 +74,32 @@ type UIConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LdapConfig struct {
|
type LdapConfig struct {
|
||||||
Address string `description:"LDAP server address." yaml:"address"`
|
Address string `description:"LDAP server address." yaml:"address"`
|
||||||
BindDN string `description:"Bind DN for LDAP authentication." yaml:"bindDn"`
|
BindDN string `description:"Bind DN for LDAP authentication." yaml:"bindDn"`
|
||||||
BindPassword string `description:"Bind password for LDAP authentication." yaml:"bindPassword"`
|
BindPassword string `description:"Bind password for LDAP authentication." yaml:"bindPassword"`
|
||||||
BaseDN string `description:"Base DN for LDAP searches." yaml:"baseDn"`
|
BaseDN string `description:"Base DN for LDAP searches." yaml:"baseDn"`
|
||||||
Insecure bool `description:"Allow insecure LDAP connections." yaml:"insecure"`
|
Insecure bool `description:"Allow insecure LDAP connections." yaml:"insecure"`
|
||||||
SearchFilter string `description:"LDAP search filter." yaml:"searchFilter"`
|
SearchFilter string `description:"LDAP search filter." yaml:"searchFilter"`
|
||||||
|
AuthCert string `description:"Certificate for mTLS authentication." yaml:"authCert"`
|
||||||
|
AuthKey string `description:"Certificate key for mTLS authentication." yaml:"authKey"`
|
||||||
|
GroupCacheTTL int `description:"Cache duration for LDAP group membership in seconds." yaml:"groupCacheTTL"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogConfig struct {
|
||||||
|
Level string `description:"Log level (trace, debug, info, warn, error)." yaml:"level"`
|
||||||
|
Json bool `description:"Enable JSON formatted logs." yaml:"json"`
|
||||||
|
Streams LogStreams `description:"Configuration for specific log streams." yaml:"streams"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogStreams struct {
|
||||||
|
HTTP LogStreamConfig `description:"HTTP request logging." yaml:"http"`
|
||||||
|
App LogStreamConfig `description:"Application logging." yaml:"app"`
|
||||||
|
Audit LogStreamConfig `description:"Audit logging." yaml:"audit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogStreamConfig struct {
|
||||||
|
Enabled bool `description:"Enable this log stream." yaml:"enabled"`
|
||||||
|
Level string `description:"Log level for this stream. Use global if empty." yaml:"level"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExperimentalConfig struct {
|
type ExperimentalConfig struct {
|
||||||
@@ -87,16 +121,25 @@ type Claims struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OAuthServiceConfig struct {
|
type OAuthServiceConfig struct {
|
||||||
ClientID string `description:"OAuth client ID."`
|
ClientID string `description:"OAuth client ID." yaml:"clientId"`
|
||||||
ClientSecret string `description:"OAuth client secret."`
|
ClientSecret string `description:"OAuth client secret." yaml:"clientSecret"`
|
||||||
ClientSecretFile string `description:"Path to the file containing the OAuth client secret."`
|
ClientSecretFile string `description:"Path to the file containing the OAuth client secret." yaml:"clientSecretFile"`
|
||||||
Scopes []string `description:"OAuth scopes."`
|
Scopes []string `description:"OAuth scopes." yaml:"scopes"`
|
||||||
RedirectURL string `description:"OAuth redirect URL."`
|
RedirectURL string `description:"OAuth redirect URL." yaml:"redirectUrl"`
|
||||||
AuthURL string `description:"OAuth authorization URL."`
|
AuthURL string `description:"OAuth authorization URL." yaml:"authUrl"`
|
||||||
TokenURL string `description:"OAuth token URL."`
|
TokenURL string `description:"OAuth token URL." yaml:"tokenUrl"`
|
||||||
UserinfoURL string `description:"OAuth userinfo URL."`
|
UserinfoURL string `description:"OAuth userinfo URL." yaml:"userinfoUrl"`
|
||||||
Insecure bool `description:"Allow insecure OAuth connections."`
|
Insecure bool `description:"Allow insecure OAuth connections." yaml:"insecure"`
|
||||||
Name string `description:"Provider name in UI."`
|
Name string `description:"Provider name in UI." yaml:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OIDCClientConfig struct {
|
||||||
|
ID string `description:"OIDC client ID." yaml:"-"`
|
||||||
|
ClientID string `description:"OIDC client ID." yaml:"clientId"`
|
||||||
|
ClientSecret string `description:"OIDC client secret." yaml:"clientSecret"`
|
||||||
|
ClientSecretFile string `description:"Path to the file containing the OIDC client secret." yaml:"clientSecretFile"`
|
||||||
|
TrustedRedirectURIs []string `description:"List of trusted redirect URIs." yaml:"trustedRedirectUris"`
|
||||||
|
Name string `description:"Client name in UI." yaml:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var OverrideProviders = map[string]string{
|
var OverrideProviders = map[string]string{
|
||||||
@@ -112,28 +155,22 @@ type User struct {
|
|||||||
TotpSecret string
|
TotpSecret string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LdapUser struct {
|
||||||
|
DN string
|
||||||
|
Groups []string
|
||||||
|
}
|
||||||
|
|
||||||
type UserSearch struct {
|
type UserSearch struct {
|
||||||
Username string
|
Username string
|
||||||
Type string // local, ldap or unknown
|
Type string // local, ldap or unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionCookie struct {
|
|
||||||
UUID string
|
|
||||||
Username string
|
|
||||||
Name string
|
|
||||||
Email string
|
|
||||||
Provider string
|
|
||||||
TotpPending bool
|
|
||||||
OAuthGroups string
|
|
||||||
OAuthName string
|
|
||||||
OAuthSub string
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserContext struct {
|
type UserContext struct {
|
||||||
Username string
|
Username string
|
||||||
Name string
|
Name string
|
||||||
Email string
|
Email string
|
||||||
IsLoggedIn bool
|
IsLoggedIn bool
|
||||||
|
IsBasicAuth bool
|
||||||
OAuth bool
|
OAuth bool
|
||||||
Provider string
|
Provider string
|
||||||
TotpPending bool
|
TotpPending bool
|
||||||
@@ -141,6 +178,7 @@ type UserContext struct {
|
|||||||
TotpEnabled bool
|
TotpEnabled bool
|
||||||
OAuthName string
|
OAuthName string
|
||||||
OAuthSub string
|
OAuthSub string
|
||||||
|
LdapGroups string
|
||||||
}
|
}
|
||||||
|
|
||||||
// API responses and queries
|
// API responses and queries
|
||||||
@@ -156,61 +194,60 @@ type RedirectQuery struct {
|
|||||||
RedirectURI string `url:"redirect_uri"`
|
RedirectURI string `url:"redirect_uri"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Labels
|
// ACLs
|
||||||
|
|
||||||
type Apps struct {
|
type Apps struct {
|
||||||
Apps map[string]App
|
Apps map[string]App `description:"App ACLs configuration." yaml:"apps"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
Config AppConfig
|
Config AppConfig `description:"App configuration." yaml:"config"`
|
||||||
Users AppUsers
|
Users AppUsers `description:"User access configuration." yaml:"users"`
|
||||||
OAuth AppOAuth
|
OAuth AppOAuth `description:"OAuth access configuration." yaml:"oauth"`
|
||||||
IP AppIP
|
IP AppIP `description:"IP access configuration." yaml:"ip"`
|
||||||
Response AppResponse
|
Response AppResponse `description:"Response customization." yaml:"response"`
|
||||||
Path AppPath
|
Path AppPath `description:"Path access configuration." yaml:"path"`
|
||||||
|
LDAP AppLDAP `description:"LDAP access configuration." yaml:"ldap"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
Domain string
|
Domain string `description:"The domain of the app." yaml:"domain"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppUsers struct {
|
type AppUsers struct {
|
||||||
Allow string
|
Allow string `description:"Comma-separated list of allowed users." yaml:"allow"`
|
||||||
Block string
|
Block string `description:"Comma-separated list of blocked users." yaml:"block"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppOAuth struct {
|
type AppOAuth struct {
|
||||||
Whitelist string
|
Whitelist string `description:"Comma-separated list of allowed OAuth groups." yaml:"whitelist"`
|
||||||
Groups string
|
Groups string `description:"Comma-separated list of required OAuth groups." yaml:"groups"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppLDAP struct {
|
||||||
|
Groups string `description:"Comma-separated list of required LDAP groups." yaml:"groups"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppIP struct {
|
type AppIP struct {
|
||||||
Allow []string
|
Allow []string `description:"List of allowed IPs or CIDR ranges." yaml:"allow"`
|
||||||
Block []string
|
Block []string `description:"List of blocked IPs or CIDR ranges." yaml:"block"`
|
||||||
Bypass []string
|
Bypass []string `description:"List of IPs or CIDR ranges that bypass authentication." yaml:"bypass"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppResponse struct {
|
type AppResponse struct {
|
||||||
Headers []string
|
Headers []string `description:"Custom headers to add to the response." yaml:"headers"`
|
||||||
BasicAuth AppBasicAuth
|
BasicAuth AppBasicAuth `description:"Basic authentication for the app." yaml:"basicAuth"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppBasicAuth struct {
|
type AppBasicAuth struct {
|
||||||
Username string
|
Username string `description:"Basic auth username." yaml:"username"`
|
||||||
Password string
|
Password string `description:"Basic auth password." yaml:"password"`
|
||||||
PasswordFile string
|
PasswordFile string `description:"Path to the file containing the basic auth password." yaml:"passwordFile"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppPath struct {
|
type AppPath struct {
|
||||||
Allow string
|
Allow string `description:"Comma-separated list of allowed paths." yaml:"allow"`
|
||||||
Block string
|
Block string `description:"Comma-separated list of blocked paths." yaml:"block"`
|
||||||
}
|
|
||||||
|
|
||||||
// Flags
|
|
||||||
|
|
||||||
type Providers struct {
|
|
||||||
Providers map[string]OAuthServiceConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// API server
|
// API server
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserContextResponse struct {
|
type UserContextResponse struct {
|
||||||
@@ -21,7 +21,6 @@ type UserContextResponse struct {
|
|||||||
OAuth bool `json:"oauth"`
|
OAuth bool `json:"oauth"`
|
||||||
TotpPending bool `json:"totpPending"`
|
TotpPending bool `json:"totpPending"`
|
||||||
OAuthName string `json:"oauthName"`
|
OAuthName string `json:"oauthName"`
|
||||||
OAuthSub string `json:"oauthSub"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppContextResponse struct {
|
type AppContextResponse struct {
|
||||||
@@ -61,7 +60,7 @@ type ContextController struct {
|
|||||||
|
|
||||||
func NewContextController(config ContextControllerConfig, router *gin.RouterGroup) *ContextController {
|
func NewContextController(config ContextControllerConfig, router *gin.RouterGroup) *ContextController {
|
||||||
if config.DisableUIWarnings {
|
if config.DisableUIWarnings {
|
||||||
log.Warn().Msg("UI warnings are disabled. This may expose users to security risks. Proceed with caution.")
|
tlog.App.Warn().Msg("UI warnings are disabled. This may expose users to security risks. Proceed with caution.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ContextController{
|
return &ContextController{
|
||||||
@@ -90,11 +89,10 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
|
|||||||
OAuth: context.OAuth,
|
OAuth: context.OAuth,
|
||||||
TotpPending: context.TotpPending,
|
TotpPending: context.TotpPending,
|
||||||
OAuthName: context.OAuthName,
|
OAuthName: context.OAuthName,
|
||||||
OAuthSub: context.OAuthSub,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug().Err(err).Msg("No user context found in request")
|
tlog.App.Debug().Err(err).Msg("No user context found in request")
|
||||||
userContext.Status = 401
|
userContext.Status = 401
|
||||||
userContext.Message = "Unauthorized"
|
userContext.Message = "Unauthorized"
|
||||||
userContext.IsLoggedIn = false
|
userContext.IsLoggedIn = false
|
||||||
@@ -108,7 +106,7 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
|
|||||||
func (controller *ContextController) appContextHandler(c *gin.Context) {
|
func (controller *ContextController) appContextHandler(c *gin.Context) {
|
||||||
appUrl, err := url.Parse(controller.config.AppURL)
|
appUrl, err := url.Parse(controller.config.AppURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to parse app URL")
|
tlog.App.Error().Err(err).Msg("Failed to parse app URL")
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, gin.H{
|
||||||
"status": 500,
|
"status": 500,
|
||||||
"message": "Internal Server Error",
|
"message": "Internal Server Error",
|
||||||
|
|||||||
@@ -7,16 +7,17 @@ import (
|
|||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
var controllerCfg = controller.ContextControllerConfig{
|
var contextControllerCfg = controller.ContextControllerConfig{
|
||||||
Providers: []controller.Provider{
|
Providers: []controller.Provider{
|
||||||
{
|
{
|
||||||
Name: "Username",
|
Name: "Local",
|
||||||
ID: "username",
|
ID: "local",
|
||||||
OAuth: false,
|
OAuth: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -34,13 +35,14 @@ var controllerCfg = controller.ContextControllerConfig{
|
|||||||
DisableUIWarnings: false,
|
DisableUIWarnings: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
var userContext = config.UserContext{
|
var contextCtrlTestContext = config.UserContext{
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Name: "testuser",
|
Name: "testuser",
|
||||||
Email: "test@example.com",
|
Email: "test@example.com",
|
||||||
IsLoggedIn: true,
|
IsLoggedIn: true,
|
||||||
|
IsBasicAuth: false,
|
||||||
OAuth: false,
|
OAuth: false,
|
||||||
Provider: "username",
|
Provider: "local",
|
||||||
TotpPending: false,
|
TotpPending: false,
|
||||||
OAuthGroups: "",
|
OAuthGroups: "",
|
||||||
TotpEnabled: false,
|
TotpEnabled: false,
|
||||||
@@ -48,6 +50,8 @@ var userContext = config.UserContext{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setupContextController(middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) {
|
func setupContextController(middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) {
|
||||||
|
tlog.NewSimpleLogger().Init()
|
||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
router := gin.Default()
|
router := gin.Default()
|
||||||
@@ -61,7 +65,7 @@ func setupContextController(middlewares *[]gin.HandlerFunc) (*gin.Engine, *httpt
|
|||||||
|
|
||||||
group := router.Group("/api")
|
group := router.Group("/api")
|
||||||
|
|
||||||
ctrl := controller.NewContextController(controllerCfg, group)
|
ctrl := controller.NewContextController(contextControllerCfg, group)
|
||||||
ctrl.SetupRoutes()
|
ctrl.SetupRoutes()
|
||||||
|
|
||||||
return router, recorder
|
return router, recorder
|
||||||
@@ -71,14 +75,14 @@ func TestAppContextHandler(t *testing.T) {
|
|||||||
expectedRes := controller.AppContextResponse{
|
expectedRes := controller.AppContextResponse{
|
||||||
Status: 200,
|
Status: 200,
|
||||||
Message: "Success",
|
Message: "Success",
|
||||||
Providers: controllerCfg.Providers,
|
Providers: contextControllerCfg.Providers,
|
||||||
Title: controllerCfg.Title,
|
Title: contextControllerCfg.Title,
|
||||||
AppURL: controllerCfg.AppURL,
|
AppURL: contextControllerCfg.AppURL,
|
||||||
CookieDomain: controllerCfg.CookieDomain,
|
CookieDomain: contextControllerCfg.CookieDomain,
|
||||||
ForgotPasswordMessage: controllerCfg.ForgotPasswordMessage,
|
ForgotPasswordMessage: contextControllerCfg.ForgotPasswordMessage,
|
||||||
BackgroundImage: controllerCfg.BackgroundImage,
|
BackgroundImage: contextControllerCfg.BackgroundImage,
|
||||||
OAuthAutoRedirect: controllerCfg.OAuthAutoRedirect,
|
OAuthAutoRedirect: contextControllerCfg.OAuthAutoRedirect,
|
||||||
DisableUIWarnings: controllerCfg.DisableUIWarnings,
|
DisableUIWarnings: contextControllerCfg.DisableUIWarnings,
|
||||||
}
|
}
|
||||||
|
|
||||||
router, recorder := setupContextController(nil)
|
router, recorder := setupContextController(nil)
|
||||||
@@ -99,20 +103,20 @@ func TestUserContextHandler(t *testing.T) {
|
|||||||
expectedRes := controller.UserContextResponse{
|
expectedRes := controller.UserContextResponse{
|
||||||
Status: 200,
|
Status: 200,
|
||||||
Message: "Success",
|
Message: "Success",
|
||||||
IsLoggedIn: userContext.IsLoggedIn,
|
IsLoggedIn: contextCtrlTestContext.IsLoggedIn,
|
||||||
Username: userContext.Username,
|
Username: contextCtrlTestContext.Username,
|
||||||
Name: userContext.Name,
|
Name: contextCtrlTestContext.Name,
|
||||||
Email: userContext.Email,
|
Email: contextCtrlTestContext.Email,
|
||||||
Provider: userContext.Provider,
|
Provider: contextCtrlTestContext.Provider,
|
||||||
OAuth: userContext.OAuth,
|
OAuth: contextCtrlTestContext.OAuth,
|
||||||
TotpPending: userContext.TotpPending,
|
TotpPending: contextCtrlTestContext.TotpPending,
|
||||||
OAuthName: userContext.OAuthName,
|
OAuthName: contextCtrlTestContext.OAuthName,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test with context
|
// Test with context
|
||||||
router, recorder := setupContextController(&[]gin.HandlerFunc{
|
router, recorder := setupContextController(&[]gin.HandlerFunc{
|
||||||
func(c *gin.Context) {
|
func(c *gin.Context) {
|
||||||
c.Set("context", &userContext)
|
c.Set("context", &contextCtrlTestContext)
|
||||||
c.Next()
|
c.Next()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/go-querystring/query"
|
"github.com/google/go-querystring/query"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type OAuthRequest struct {
|
type OAuthRequest struct {
|
||||||
@@ -54,7 +55,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
|||||||
|
|
||||||
err := c.BindUri(&req)
|
err := c.BindUri(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to bind URI")
|
tlog.App.Error().Err(err).Msg("Failed to bind URI")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, gin.H{
|
||||||
"status": 400,
|
"status": 400,
|
||||||
"message": "Bad Request",
|
"message": "Bad Request",
|
||||||
@@ -65,7 +66,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
|||||||
service, exists := controller.broker.GetService(req.Provider)
|
service, exists := controller.broker.GetService(req.Provider)
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
log.Warn().Msgf("OAuth provider not found: %s", req.Provider)
|
tlog.App.Warn().Msgf("OAuth provider not found: %s", req.Provider)
|
||||||
c.JSON(404, gin.H{
|
c.JSON(404, gin.H{
|
||||||
"status": 404,
|
"status": 404,
|
||||||
"message": "Not Found",
|
"message": "Not Found",
|
||||||
@@ -82,12 +83,12 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
|||||||
isRedirectSafe := utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain)
|
isRedirectSafe := utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain)
|
||||||
|
|
||||||
if !isRedirectSafe {
|
if !isRedirectSafe {
|
||||||
log.Warn().Str("redirect_uri", redirectURI).Msg("Unsafe redirect URI detected, ignoring")
|
tlog.App.Warn().Str("redirect_uri", redirectURI).Msg("Unsafe redirect URI detected, ignoring")
|
||||||
redirectURI = ""
|
redirectURI = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if redirectURI != "" && isRedirectSafe {
|
if redirectURI != "" && isRedirectSafe {
|
||||||
log.Debug().Msg("Setting redirect URI cookie")
|
tlog.App.Debug().Msg("Setting redirect URI cookie")
|
||||||
c.SetCookie(controller.config.RedirectCookieName, redirectURI, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
|
c.SetCookie(controller.config.RedirectCookieName, redirectURI, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +104,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
err := c.BindUri(&req)
|
err := c.BindUri(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to bind URI")
|
tlog.App.Error().Err(err).Msg("Failed to bind URI")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, gin.H{
|
||||||
"status": 400,
|
"status": 400,
|
||||||
"message": "Bad Request",
|
"message": "Bad Request",
|
||||||
@@ -115,7 +116,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
csrfCookie, err := c.Cookie(controller.config.CSRFCookieName)
|
csrfCookie, err := c.Cookie(controller.config.CSRFCookieName)
|
||||||
|
|
||||||
if err != nil || state != csrfCookie {
|
if err != nil || state != csrfCookie {
|
||||||
log.Warn().Err(err).Msg("CSRF token mismatch or cookie missing")
|
tlog.App.Warn().Err(err).Msg("CSRF token mismatch or cookie missing")
|
||||||
c.SetCookie(controller.config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
|
c.SetCookie(controller.config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
@@ -127,14 +128,14 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
service, exists := controller.broker.GetService(req.Provider)
|
service, exists := controller.broker.GetService(req.Provider)
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
log.Warn().Msgf("OAuth provider not found: %s", req.Provider)
|
tlog.App.Warn().Msgf("OAuth provider not found: %s", req.Provider)
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = service.VerifyCode(code)
|
err = service.VerifyCode(code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to verify OAuth code")
|
tlog.App.Error().Err(err).Msg("Failed to verify OAuth code")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -142,26 +143,27 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
user, err := controller.broker.GetUser(req.Provider)
|
user, err := controller.broker.GetUser(req.Provider)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to get user from OAuth provider")
|
tlog.App.Error().Err(err).Msg("Failed to get user from OAuth provider")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.Email == "" {
|
if user.Email == "" {
|
||||||
log.Error().Msg("OAuth provider did not return an email")
|
tlog.App.Error().Msg("OAuth provider did not return an email")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !controller.auth.IsEmailWhitelisted(user.Email) {
|
if !controller.auth.IsEmailWhitelisted(user.Email) {
|
||||||
log.Warn().Str("email", user.Email).Msg("Email not whitelisted")
|
tlog.App.Warn().Str("email", user.Email).Msg("Email not whitelisted")
|
||||||
|
tlog.AuditLoginFailure(c, user.Email, req.Provider, "email not whitelisted")
|
||||||
|
|
||||||
queries, err := query.Values(config.UnauthorizedQuery{
|
queries, err := query.Values(config.UnauthorizedQuery{
|
||||||
Username: user.Email,
|
Username: user.Email,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to encode unauthorized query")
|
tlog.App.Error().Err(err).Msg("Failed to encode unauthorized query")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -173,24 +175,24 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
var name string
|
var name string
|
||||||
|
|
||||||
if strings.TrimSpace(user.Name) != "" {
|
if strings.TrimSpace(user.Name) != "" {
|
||||||
log.Debug().Msg("Using name from OAuth provider")
|
tlog.App.Debug().Msg("Using name from OAuth provider")
|
||||||
name = user.Name
|
name = user.Name
|
||||||
} else {
|
} else {
|
||||||
log.Debug().Msg("No name from OAuth provider, using pseudo name")
|
tlog.App.Debug().Msg("No name from OAuth provider, using pseudo name")
|
||||||
name = fmt.Sprintf("%s (%s)", utils.Capitalize(strings.Split(user.Email, "@")[0]), strings.Split(user.Email, "@")[1])
|
name = fmt.Sprintf("%s (%s)", utils.Capitalize(strings.Split(user.Email, "@")[0]), strings.Split(user.Email, "@")[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
var username string
|
var username string
|
||||||
|
|
||||||
if strings.TrimSpace(user.PreferredUsername) != "" {
|
if strings.TrimSpace(user.PreferredUsername) != "" {
|
||||||
log.Debug().Msg("Using preferred username from OAuth provider")
|
tlog.App.Debug().Msg("Using preferred username from OAuth provider")
|
||||||
username = user.PreferredUsername
|
username = user.PreferredUsername
|
||||||
} else {
|
} else {
|
||||||
log.Debug().Msg("No preferred username from OAuth provider, using pseudo username")
|
tlog.App.Debug().Msg("No preferred username from OAuth provider, using pseudo username")
|
||||||
username = strings.Replace(user.Email, "@", "_", -1)
|
username = strings.Replace(user.Email, "@", "_", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionCookie := config.SessionCookie{
|
sessionCookie := repository.Session{
|
||||||
Username: username,
|
Username: username,
|
||||||
Name: name,
|
Name: name,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
@@ -200,20 +202,22 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
OAuthSub: user.Sub,
|
OAuthSub: user.Sub,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
||||||
|
|
||||||
err = controller.auth.CreateSessionCookie(c, &sessionCookie)
|
err = controller.auth.CreateSessionCookie(c, &sessionCookie)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to create session cookie")
|
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tlog.AuditLoginSuccess(c, sessionCookie.Username, sessionCookie.Provider)
|
||||||
|
|
||||||
redirectURI, err := c.Cookie(controller.config.RedirectCookieName)
|
redirectURI, err := c.Cookie(controller.config.RedirectCookieName)
|
||||||
|
|
||||||
if err != nil || !utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain) {
|
if err != nil || !utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain) {
|
||||||
log.Debug().Msg("No redirect URI cookie found, redirecting to app root")
|
tlog.App.Debug().Msg("No redirect URI cookie found, redirecting to app root")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, controller.config.AppURL)
|
c.Redirect(http.StatusTemporaryRedirect, controller.config.AppURL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -223,7 +227,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to encode redirect URI query")
|
tlog.App.Error().Err(err).Msg("Failed to encode redirect URI query")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
458
internal/controller/oidc_controller.go
Normal file
458
internal/controller/oidc_controller.go
Normal file
@@ -0,0 +1,458 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/go-querystring/query"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OIDCControllerConfig struct{}
|
||||||
|
|
||||||
|
type OIDCController struct {
|
||||||
|
config OIDCControllerConfig
|
||||||
|
router *gin.RouterGroup
|
||||||
|
oidc *service.OIDCService
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthorizeCallback struct {
|
||||||
|
Code string `url:"code"`
|
||||||
|
State string `url:"state"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenRequest struct {
|
||||||
|
GrantType string `form:"grant_type" binding:"required" url:"grant_type"`
|
||||||
|
Code string `form:"code" url:"code"`
|
||||||
|
RedirectURI string `form:"redirect_uri" url:"redirect_uri"`
|
||||||
|
RefreshToken string `form:"refresh_token" url:"refresh_token"`
|
||||||
|
ClientSecret string `form:"client_secret" url:"client_secret"`
|
||||||
|
ClientID string `form:"client_id" url:"client_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CallbackError struct {
|
||||||
|
Error string `url:"error"`
|
||||||
|
ErrorDescription string `url:"error_description"`
|
||||||
|
State string `url:"state"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorScreen struct {
|
||||||
|
Error string `url:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientRequest struct {
|
||||||
|
ClientID string `uri:"id" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientCredentials struct {
|
||||||
|
ClientID string
|
||||||
|
ClientSecret string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOIDCController(config OIDCControllerConfig, oidcService *service.OIDCService, router *gin.RouterGroup) *OIDCController {
|
||||||
|
return &OIDCController{
|
||||||
|
config: config,
|
||||||
|
oidc: oidcService,
|
||||||
|
router: router,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (controller *OIDCController) SetupRoutes() {
|
||||||
|
oidcGroup := controller.router.Group("/oidc")
|
||||||
|
oidcGroup.GET("/clients/:id", controller.GetClientInfo)
|
||||||
|
oidcGroup.POST("/authorize", controller.Authorize)
|
||||||
|
oidcGroup.POST("/token", controller.Token)
|
||||||
|
oidcGroup.GET("/userinfo", controller.Userinfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (controller *OIDCController) GetClientInfo(c *gin.Context) {
|
||||||
|
var req ClientRequest
|
||||||
|
|
||||||
|
err := c.BindUri(&req)
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Error().Err(err).Msg("Failed to bind URI")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": 400,
|
||||||
|
"message": "Bad Request",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client, ok := controller.oidc.GetClient(req.ClientID)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
tlog.App.Warn().Str("client_id", req.ClientID).Msg("Client not found")
|
||||||
|
c.JSON(404, gin.H{
|
||||||
|
"status": 404,
|
||||||
|
"message": "Client not found",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": 200,
|
||||||
|
"client": client.ClientID,
|
||||||
|
"name": client.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (controller *OIDCController) Authorize(c *gin.Context) {
|
||||||
|
if !controller.oidc.IsConfigured() {
|
||||||
|
controller.authorizeError(c, errors.New("err_oidc_not_configured"), "OIDC not configured", "This instance is not configured for OIDC", "", "", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userContext, err := utils.GetContext(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
controller.authorizeError(c, err, "Failed to get user context", "User is not logged in or the session is invalid", "", "", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req service.AuthorizeRequest
|
||||||
|
|
||||||
|
err = c.BindJSON(&req)
|
||||||
|
if err != nil {
|
||||||
|
controller.authorizeError(c, err, "Failed to bind JSON", "The client provided an invalid authorization request", "", "", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client, ok := controller.oidc.GetClient(req.ClientID)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
controller.authorizeError(c, err, "Client not found", "The client ID is invalid", "", "", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = controller.oidc.ValidateAuthorizeParams(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Error().Err(err).Msg("Failed to validate authorize params")
|
||||||
|
if err.Error() != "invalid_request_uri" {
|
||||||
|
controller.authorizeError(c, err, "Failed validate authorize params", "Invalid request parameters", req.RedirectURI, err.Error(), req.State)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
controller.authorizeError(c, err, "Redirect URI not trusted", "The provided redirect URI is not trusted", "", "", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// WARNING: Since Tinyauth is stateless, we cannot have a sub that never changes. We will just create a uuid out of the username and client name which remains stable, but if username or client name changes then sub changes too.
|
||||||
|
sub := utils.GenerateUUID(fmt.Sprintf("%s:%s", userContext.Username, client.ID))
|
||||||
|
code := rand.Text()
|
||||||
|
|
||||||
|
// Before storing the code, delete old session
|
||||||
|
err = controller.oidc.DeleteOldSession(c, sub)
|
||||||
|
if err != nil {
|
||||||
|
controller.authorizeError(c, err, "Failed to delete old sessions", "Failed to delete old sessions", req.RedirectURI, "server_error", req.State)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = controller.oidc.StoreCode(c, sub, code, req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
controller.authorizeError(c, err, "Failed to store code", "Failed to store code", req.RedirectURI, "server_error", req.State)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We also need a snapshot of the user that authorized this (skip if no openid scope)
|
||||||
|
if slices.Contains(strings.Fields(req.Scope), "openid") {
|
||||||
|
err = controller.oidc.StoreUserinfo(c, sub, userContext, req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Error().Err(err).Msg("Failed to insert user info into database")
|
||||||
|
controller.authorizeError(c, err, "Failed to store user info", "Failed to store user info", req.RedirectURI, "server_error", req.State)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
queries, err := query.Values(AuthorizeCallback{
|
||||||
|
Code: code,
|
||||||
|
State: req.State,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
controller.authorizeError(c, err, "Failed to build query", "Failed to build query", req.RedirectURI, "server_error", req.State)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": 200,
|
||||||
|
"redirect_uri": fmt.Sprintf("%s?%s", req.RedirectURI, queries.Encode()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (controller *OIDCController) Token(c *gin.Context) {
|
||||||
|
if !controller.oidc.IsConfigured() {
|
||||||
|
tlog.App.Warn().Msg("OIDC not configured")
|
||||||
|
c.JSON(404, gin.H{
|
||||||
|
"error": "not_found",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req TokenRequest
|
||||||
|
|
||||||
|
err := c.Bind(&req)
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Error().Err(err).Msg("Failed to bind token request")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"error": "invalid_request",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = controller.oidc.ValidateGrantType(req.GrantType)
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Warn().Str("grant_type", req.GrantType).Msg("Unsupported grant type")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// First we try form values
|
||||||
|
creds := ClientCredentials{
|
||||||
|
ClientID: req.ClientID,
|
||||||
|
ClientSecret: req.ClientSecret,
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it fails, we try basic auth
|
||||||
|
if creds.ClientID == "" || creds.ClientSecret == "" {
|
||||||
|
tlog.App.Debug().Msg("Tried form values and they are empty, trying basic auth")
|
||||||
|
|
||||||
|
clientId, clientSecret, ok := c.Request.BasicAuth()
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
tlog.App.Error().Msg("Missing authorization header")
|
||||||
|
c.Header("www-authenticate", "basic")
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"error": "invalid_client",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
creds.ClientID = clientId
|
||||||
|
creds.ClientSecret = clientSecret
|
||||||
|
}
|
||||||
|
|
||||||
|
// END - we don't support other authentication methods
|
||||||
|
|
||||||
|
client, ok := controller.oidc.GetClient(creds.ClientID)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
tlog.App.Warn().Str("client_id", creds.ClientID).Msg("Client not found")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"error": "invalid_client",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.ClientSecret != creds.ClientSecret {
|
||||||
|
tlog.App.Warn().Str("client_id", creds.ClientID).Msg("Invalid client secret")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"error": "invalid_client",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenResponse service.TokenResponse
|
||||||
|
|
||||||
|
switch req.GrantType {
|
||||||
|
case "authorization_code":
|
||||||
|
entry, err := controller.oidc.GetCodeEntry(c, controller.oidc.Hash(req.Code))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, service.ErrCodeNotFound) {
|
||||||
|
tlog.App.Warn().Msg("Code not found")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"error": "invalid_grant",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if errors.Is(err, service.ErrCodeExpired) {
|
||||||
|
tlog.App.Warn().Msg("Code expired")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"error": "invalid_grant",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tlog.App.Warn().Err(err).Msg("Failed to get OIDC code entry")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"error": "server_error",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.RedirectURI != req.RedirectURI {
|
||||||
|
tlog.App.Warn().Str("redirect_uri", req.RedirectURI).Msg("Redirect URI mismatch")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"error": "invalid_grant",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenRes, err := controller.oidc.GenerateAccessToken(c, client, entry.Sub, entry.Scope)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Error().Err(err).Msg("Failed to generate access token")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"error": "server_error",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenResponse = tokenRes
|
||||||
|
case "refresh_token":
|
||||||
|
tokenRes, err := controller.oidc.RefreshAccessToken(c, req.RefreshToken, creds.ClientID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, service.ErrTokenExpired) {
|
||||||
|
tlog.App.Error().Err(err).Msg("Refresh token expired")
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"error": "invalid_grant",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, service.ErrInvalidClient) {
|
||||||
|
tlog.App.Error().Err(err).Msg("Invalid client")
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"error": "invalid_grant",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tlog.App.Error().Err(err).Msg("Failed to refresh access token")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"error": "server_error",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenResponse = tokenRes
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, tokenResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (controller *OIDCController) Userinfo(c *gin.Context) {
|
||||||
|
if !controller.oidc.IsConfigured() {
|
||||||
|
tlog.App.Warn().Msg("OIDC not configured")
|
||||||
|
c.JSON(404, gin.H{
|
||||||
|
"error": "not_found",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authorization := c.GetHeader("Authorization")
|
||||||
|
|
||||||
|
tokenType, token, ok := strings.Cut(authorization, " ")
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
tlog.App.Warn().Msg("OIDC userinfo accessed without authorization header")
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"error": "invalid_grant",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.ToLower(tokenType) != "bearer" {
|
||||||
|
tlog.App.Warn().Msg("OIDC userinfo accessed with invalid token type")
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"error": "invalid_grant",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entry, err := controller.oidc.GetAccessToken(c, controller.oidc.Hash(token))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == service.ErrTokenNotFound {
|
||||||
|
tlog.App.Warn().Msg("OIDC userinfo accessed with invalid token")
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"error": "invalid_grant",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tlog.App.Err(err).Msg("Failed to get token entry")
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"error": "server_error",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't have the openid scope, return an error
|
||||||
|
if !slices.Contains(strings.Split(entry.Scope, ","), "openid") {
|
||||||
|
tlog.App.Warn().Msg("OIDC userinfo accessed without openid scope")
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"error": "invalid_scope",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := controller.oidc.GetUserinfo(c, entry.Sub)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Err(err).Msg("Failed to get user entry")
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"error": "server_error",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, controller.oidc.CompileUserinfo(user, entry.Scope))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (controller *OIDCController) authorizeError(c *gin.Context, err error, reason string, reasonUser string, callback string, callbackError string, state string) {
|
||||||
|
tlog.App.Error().Err(err).Msg(reason)
|
||||||
|
|
||||||
|
if callback != "" {
|
||||||
|
errorQueries := CallbackError{
|
||||||
|
Error: callbackError,
|
||||||
|
}
|
||||||
|
|
||||||
|
if reasonUser != "" {
|
||||||
|
errorQueries.ErrorDescription = reasonUser
|
||||||
|
}
|
||||||
|
|
||||||
|
if state != "" {
|
||||||
|
errorQueries.State = state
|
||||||
|
}
|
||||||
|
|
||||||
|
queries, err := query.Values(errorQueries)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": 200,
|
||||||
|
"redirect_uri": fmt.Sprintf("%s?%s", callback, queries.Encode()),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
errorQueries := ErrorScreen{
|
||||||
|
Error: reasonUser,
|
||||||
|
}
|
||||||
|
|
||||||
|
queries, err := query.Values(errorQueries)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": 200,
|
||||||
|
"redirect_uri": fmt.Sprintf("%s/error?%s", controller.oidc.GetIssuer(), queries.Encode()),
|
||||||
|
})
|
||||||
|
}
|
||||||
281
internal/controller/oidc_controller_test.go
Normal file
281
internal/controller/oidc_controller_test.go
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
package controller_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/go-querystring/query"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/bootstrap"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var oidcServiceConfig = service.OIDCServiceConfig{
|
||||||
|
Clients: map[string]config.OIDCClientConfig{
|
||||||
|
"client1": {
|
||||||
|
ClientID: "some-client-id",
|
||||||
|
ClientSecret: "some-client-secret",
|
||||||
|
ClientSecretFile: "",
|
||||||
|
TrustedRedirectURIs: []string{
|
||||||
|
"https://example.com/oauth/callback",
|
||||||
|
},
|
||||||
|
Name: "Client 1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PrivateKeyPath: "/tmp/tinyauth_oidc_key",
|
||||||
|
PublicKeyPath: "/tmp/tinyauth_oidc_key.pub",
|
||||||
|
Issuer: "https://example.com",
|
||||||
|
SessionExpiry: 3600,
|
||||||
|
}
|
||||||
|
|
||||||
|
var oidcCtrlTestContext = config.UserContext{
|
||||||
|
Username: "test",
|
||||||
|
Name: "Test",
|
||||||
|
Email: "test@example.com",
|
||||||
|
IsLoggedIn: true,
|
||||||
|
IsBasicAuth: false,
|
||||||
|
OAuth: false,
|
||||||
|
Provider: "ldap", // ldap in order to test the groups
|
||||||
|
TotpPending: false,
|
||||||
|
OAuthGroups: "",
|
||||||
|
TotpEnabled: false,
|
||||||
|
OAuthName: "",
|
||||||
|
OAuthSub: "",
|
||||||
|
LdapGroups: "test1,test2",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test is not amazing, but it will confirm the OIDC server works
|
||||||
|
func TestOIDCController(t *testing.T) {
|
||||||
|
tlog.NewSimpleLogger().Init()
|
||||||
|
|
||||||
|
// Create an app instance
|
||||||
|
app := bootstrap.NewBootstrapApp(config.Config{})
|
||||||
|
|
||||||
|
// Get db
|
||||||
|
db, err := app.SetupDatabase("/tmp/tinyauth.db")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
// Create queries
|
||||||
|
queries := repository.New(db)
|
||||||
|
|
||||||
|
// Create a new OIDC Servicee
|
||||||
|
oidcService := service.NewOIDCService(oidcServiceConfig, queries)
|
||||||
|
err = oidcService.Init()
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
// Create test router
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
router := gin.Default()
|
||||||
|
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Set("context", &oidcCtrlTestContext)
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
group := router.Group("/api")
|
||||||
|
|
||||||
|
// Register oidc controller
|
||||||
|
oidcController := controller.NewOIDCController(controller.OIDCControllerConfig{}, oidcService, group)
|
||||||
|
oidcController.SetupRoutes()
|
||||||
|
|
||||||
|
// Get redirect URL test
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
marshalled, err := json.Marshal(service.AuthorizeRequest{
|
||||||
|
Scope: "openid profile email groups",
|
||||||
|
ResponseType: "code",
|
||||||
|
ClientID: "some-client-id",
|
||||||
|
RedirectURI: "https://example.com/oauth/callback",
|
||||||
|
State: "some-state",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", "/api/oidc/authorize", strings.NewReader(string(marshalled)))
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
|
||||||
|
resJson := map[string]any{}
|
||||||
|
|
||||||
|
err = json.Unmarshal(recorder.Body.Bytes(), &resJson)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
redirect_uri, ok := resJson["redirect_uri"].(string)
|
||||||
|
assert.Assert(t, ok)
|
||||||
|
|
||||||
|
u, err := url.Parse(redirect_uri)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
m, err := url.ParseQuery(u.RawQuery)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Equal(t, m["state"][0], "some-state")
|
||||||
|
|
||||||
|
code := m["code"][0]
|
||||||
|
|
||||||
|
// Exchange code for token
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
|
||||||
|
params, err := query.Values(controller.TokenRequest{
|
||||||
|
GrantType: "authorization_code",
|
||||||
|
Code: code,
|
||||||
|
RedirectURI: "https://example.com/oauth/callback",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
req, err = http.NewRequest("POST", "/api/oidc/token", strings.NewReader(params.Encode()))
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
req.Header.Set("content-type", "application/x-www-form-urlencoded")
|
||||||
|
req.SetBasicAuth("some-client-id", "some-client-secret")
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
|
||||||
|
resJson = map[string]any{}
|
||||||
|
|
||||||
|
err = json.Unmarshal(recorder.Body.Bytes(), &resJson)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
accessToken, ok := resJson["access_token"].(string)
|
||||||
|
assert.Assert(t, ok)
|
||||||
|
|
||||||
|
_, ok = resJson["id_token"].(string)
|
||||||
|
assert.Assert(t, ok)
|
||||||
|
|
||||||
|
refreshToken, ok := resJson["refresh_token"].(string)
|
||||||
|
assert.Assert(t, ok)
|
||||||
|
|
||||||
|
expires_in, ok := resJson["expires_in"].(float64)
|
||||||
|
assert.Assert(t, ok)
|
||||||
|
assert.Equal(t, expires_in, float64(oidcServiceConfig.SessionExpiry))
|
||||||
|
|
||||||
|
// Ensure code is expired
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
|
||||||
|
params, err = query.Values(controller.TokenRequest{
|
||||||
|
GrantType: "authorization_code",
|
||||||
|
Code: code,
|
||||||
|
RedirectURI: "https://example.com/oauth/callback",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
req, err = http.NewRequest("POST", "/api/oidc/token", strings.NewReader(params.Encode()))
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
req.Header.Set("content-type", "application/x-www-form-urlencoded")
|
||||||
|
req.SetBasicAuth("some-client-id", "some-client-secret")
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, recorder.Code)
|
||||||
|
|
||||||
|
// Test userinfo
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
|
||||||
|
req, err = http.NewRequest("GET", "/api/oidc/userinfo", nil)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
req.Header.Set("authorization", fmt.Sprintf("Bearer %s", accessToken))
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
|
||||||
|
resJson = map[string]any{}
|
||||||
|
|
||||||
|
err = json.Unmarshal(recorder.Body.Bytes(), &resJson)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
_, ok = resJson["sub"].(string)
|
||||||
|
assert.Assert(t, ok)
|
||||||
|
|
||||||
|
name, ok := resJson["name"].(string)
|
||||||
|
assert.Assert(t, ok)
|
||||||
|
assert.Equal(t, name, oidcCtrlTestContext.Name)
|
||||||
|
|
||||||
|
email, ok := resJson["email"].(string)
|
||||||
|
assert.Assert(t, ok)
|
||||||
|
assert.Equal(t, email, oidcCtrlTestContext.Email)
|
||||||
|
|
||||||
|
preferred_username, ok := resJson["preferred_username"].(string)
|
||||||
|
assert.Assert(t, ok)
|
||||||
|
assert.Equal(t, preferred_username, oidcCtrlTestContext.Username)
|
||||||
|
|
||||||
|
// Not sure why this is failing, will look into it later
|
||||||
|
igroups, ok := resJson["groups"].([]any)
|
||||||
|
assert.Assert(t, ok)
|
||||||
|
|
||||||
|
groups := make([]string, len(igroups))
|
||||||
|
for i, group := range igroups {
|
||||||
|
groups[i], ok = group.(string)
|
||||||
|
assert.Assert(t, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.DeepEqual(t, strings.Split(oidcCtrlTestContext.LdapGroups, ","), groups)
|
||||||
|
|
||||||
|
// Test refresh token
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
|
||||||
|
params, err = query.Values(controller.TokenRequest{
|
||||||
|
GrantType: "refresh_token",
|
||||||
|
RefreshToken: refreshToken,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
req, err = http.NewRequest("POST", "/api/oidc/token", strings.NewReader(params.Encode()))
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.SetBasicAuth("some-client-id", "some-client-secret")
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
|
||||||
|
resJson = map[string]any{}
|
||||||
|
|
||||||
|
err = json.Unmarshal(recorder.Body.Bytes(), &resJson)
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
newToken, ok := resJson["access_token"].(string)
|
||||||
|
assert.Assert(t, ok)
|
||||||
|
assert.Assert(t, newToken != accessToken)
|
||||||
|
|
||||||
|
// Ensure old token is invalid
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
req, err = http.NewRequest("GET", "/api/oidc/userinfo", nil)
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
req.Header.Set("authorization", fmt.Sprintf("Bearer %s", accessToken))
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, recorder.Code)
|
||||||
|
|
||||||
|
// Test new token
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
req, err = http.NewRequest("GET", "/api/oidc/userinfo", nil)
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
req.Header.Set("authorization", fmt.Sprintf("Bearer %s", newToken))
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
}
|
||||||
@@ -9,10 +9,10 @@ import (
|
|||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/go-querystring/query"
|
"github.com/google/go-querystring/query"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var SupportedProxies = []string{"nginx", "traefik", "caddy", "envoy"}
|
var SupportedProxies = []string{"nginx", "traefik", "caddy", "envoy"}
|
||||||
@@ -43,8 +43,8 @@ func NewProxyController(config ProxyControllerConfig, router *gin.RouterGroup, a
|
|||||||
|
|
||||||
func (controller *ProxyController) SetupRoutes() {
|
func (controller *ProxyController) SetupRoutes() {
|
||||||
proxyGroup := controller.router.Group("/auth")
|
proxyGroup := controller.router.Group("/auth")
|
||||||
proxyGroup.GET("/:proxy", controller.proxyHandler)
|
// There is a later check to control allowed methods per proxy
|
||||||
proxyGroup.POST("/:proxy", controller.proxyHandler)
|
proxyGroup.Any("/:proxy", controller.proxyHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||||
@@ -52,7 +52,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
|
|
||||||
err := c.BindUri(&req)
|
err := c.BindUri(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to bind URI")
|
tlog.App.Error().Err(err).Msg("Failed to bind URI")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, gin.H{
|
||||||
"status": 400,
|
"status": 400,
|
||||||
"message": "Bad Request",
|
"message": "Bad Request",
|
||||||
@@ -61,7 +61,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !slices.Contains(SupportedProxies, req.Proxy) {
|
if !slices.Contains(SupportedProxies, req.Proxy) {
|
||||||
log.Warn().Str("proxy", req.Proxy).Msg("Invalid proxy")
|
tlog.App.Warn().Str("proxy", req.Proxy).Msg("Invalid proxy")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, gin.H{
|
||||||
"status": 400,
|
"status": 400,
|
||||||
"message": "Bad Request",
|
"message": "Bad Request",
|
||||||
@@ -69,12 +69,25 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only allow GET for non-envoy proxies.
|
||||||
|
// Envoy uses the original client method for the external auth request
|
||||||
|
// so we allow Any standard HTTP method for /api/auth/envoy
|
||||||
|
if req.Proxy != "envoy" && c.Request.Method != http.MethodGet {
|
||||||
|
tlog.App.Warn().Str("method", c.Request.Method).Msg("Invalid method for proxy")
|
||||||
|
c.Header("Allow", "GET")
|
||||||
|
c.JSON(405, gin.H{
|
||||||
|
"status": 405,
|
||||||
|
"message": "Method Not Allowed",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
isBrowser := strings.Contains(c.Request.Header.Get("Accept"), "text/html")
|
isBrowser := strings.Contains(c.Request.Header.Get("Accept"), "text/html")
|
||||||
|
|
||||||
if isBrowser {
|
if isBrowser {
|
||||||
log.Debug().Msg("Request identified as (most likely) coming from a browser")
|
tlog.App.Debug().Msg("Request identified as (most likely) coming from a browser")
|
||||||
} else {
|
} else {
|
||||||
log.Debug().Msg("Request identified as (most likely) coming from a non-browser client")
|
tlog.App.Debug().Msg("Request identified as (most likely) coming from a non-browser client")
|
||||||
}
|
}
|
||||||
|
|
||||||
uri := c.Request.Header.Get("X-Forwarded-Uri")
|
uri := c.Request.Header.Get("X-Forwarded-Uri")
|
||||||
@@ -85,12 +98,12 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
acls, err := controller.acls.GetAccessControls(host)
|
acls, err := controller.acls.GetAccessControls(host)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to get access controls for resource")
|
tlog.App.Error().Err(err).Msg("Failed to get access controls for resource")
|
||||||
controller.handleError(c, req, isBrowser)
|
controller.handleError(c, req, isBrowser)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace().Interface("acls", acls).Msg("ACLs for resource")
|
tlog.App.Trace().Interface("acls", acls).Msg("ACLs for resource")
|
||||||
|
|
||||||
clientIP := c.ClientIP()
|
clientIP := c.ClientIP()
|
||||||
|
|
||||||
@@ -106,13 +119,13 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
authEnabled, err := controller.auth.IsAuthEnabled(uri, acls.Path)
|
authEnabled, err := controller.auth.IsAuthEnabled(uri, acls.Path)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to check if auth is enabled for resource")
|
tlog.App.Error().Err(err).Msg("Failed to check if auth is enabled for resource")
|
||||||
controller.handleError(c, req, isBrowser)
|
controller.handleError(c, req, isBrowser)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !authEnabled {
|
if !authEnabled {
|
||||||
log.Debug().Msg("Authentication disabled for resource, allowing access")
|
tlog.App.Debug().Msg("Authentication disabled for resource, allowing access")
|
||||||
controller.setHeaders(c, acls)
|
controller.setHeaders(c, acls)
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
@@ -136,7 +149,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to encode unauthorized query")
|
tlog.App.Error().Err(err).Msg("Failed to encode unauthorized query")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -150,7 +163,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
context, err := utils.GetContext(c)
|
context, err := utils.GetContext(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug().Msg("No user context found in request, treating as not logged in")
|
tlog.App.Debug().Msg("No user context found in request, treating as not logged in")
|
||||||
userContext = config.UserContext{
|
userContext = config.UserContext{
|
||||||
IsLoggedIn: false,
|
IsLoggedIn: false,
|
||||||
}
|
}
|
||||||
@@ -158,18 +171,18 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
userContext = context
|
userContext = context
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace().Interface("context", userContext).Msg("User context from request")
|
tlog.App.Trace().Interface("context", userContext).Msg("User context from request")
|
||||||
|
|
||||||
if userContext.Provider == "basic" && userContext.TotpEnabled {
|
if userContext.IsBasicAuth && userContext.TotpEnabled {
|
||||||
log.Debug().Msg("User has TOTP enabled, denying basic auth access")
|
tlog.App.Debug().Msg("User has TOTP enabled, denying basic auth access")
|
||||||
userContext.IsLoggedIn = false
|
userContext.IsLoggedIn = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if userContext.IsLoggedIn {
|
if userContext.IsLoggedIn {
|
||||||
appAllowed := controller.auth.IsResourceAllowed(c, userContext, acls)
|
userAllowed := controller.auth.IsUserAllowed(c, userContext, acls)
|
||||||
|
|
||||||
if !appAllowed {
|
if !userAllowed {
|
||||||
log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource")
|
tlog.App.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource")
|
||||||
|
|
||||||
if req.Proxy == "nginx" || !isBrowser {
|
if req.Proxy == "nginx" || !isBrowser {
|
||||||
c.JSON(403, gin.H{
|
c.JSON(403, gin.H{
|
||||||
@@ -184,7 +197,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to encode unauthorized query")
|
tlog.App.Error().Err(err).Msg("Failed to encode unauthorized query")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -199,11 +212,17 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if userContext.OAuth {
|
if userContext.OAuth || userContext.Provider == "ldap" {
|
||||||
groupOK := controller.auth.IsInOAuthGroup(c, userContext, acls.OAuth.Groups)
|
var groupOK bool
|
||||||
|
|
||||||
|
if userContext.OAuth {
|
||||||
|
groupOK = controller.auth.IsInOAuthGroup(c, userContext, acls.OAuth.Groups)
|
||||||
|
} else {
|
||||||
|
groupOK = controller.auth.IsInLdapGroup(c, userContext, acls.LDAP.Groups)
|
||||||
|
}
|
||||||
|
|
||||||
if !groupOK {
|
if !groupOK {
|
||||||
log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements")
|
tlog.App.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User groups do not match resource requirements")
|
||||||
|
|
||||||
if req.Proxy == "nginx" || !isBrowser {
|
if req.Proxy == "nginx" || !isBrowser {
|
||||||
c.JSON(403, gin.H{
|
c.JSON(403, gin.H{
|
||||||
@@ -219,7 +238,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to encode unauthorized query")
|
tlog.App.Error().Err(err).Msg("Failed to encode unauthorized query")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -238,7 +257,13 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
c.Header("Remote-User", utils.SanitizeHeader(userContext.Username))
|
c.Header("Remote-User", utils.SanitizeHeader(userContext.Username))
|
||||||
c.Header("Remote-Name", utils.SanitizeHeader(userContext.Name))
|
c.Header("Remote-Name", utils.SanitizeHeader(userContext.Name))
|
||||||
c.Header("Remote-Email", utils.SanitizeHeader(userContext.Email))
|
c.Header("Remote-Email", utils.SanitizeHeader(userContext.Email))
|
||||||
c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups))
|
|
||||||
|
if userContext.Provider == "ldap" {
|
||||||
|
c.Header("Remote-Groups", utils.SanitizeHeader(userContext.LdapGroups))
|
||||||
|
} else if userContext.Provider != "local" {
|
||||||
|
c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups))
|
||||||
|
}
|
||||||
|
|
||||||
c.Header("Remote-Sub", utils.SanitizeHeader(userContext.OAuthSub))
|
c.Header("Remote-Sub", utils.SanitizeHeader(userContext.OAuthSub))
|
||||||
|
|
||||||
controller.setHeaders(c, acls)
|
controller.setHeaders(c, acls)
|
||||||
@@ -263,7 +288,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to encode redirect URI query")
|
tlog.App.Error().Err(err).Msg("Failed to encode redirect URI query")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -277,14 +302,14 @@ func (controller *ProxyController) setHeaders(c *gin.Context, acls config.App) {
|
|||||||
headers := utils.ParseHeaders(acls.Response.Headers)
|
headers := utils.ParseHeaders(acls.Response.Headers)
|
||||||
|
|
||||||
for key, value := range headers {
|
for key, value := range headers {
|
||||||
log.Debug().Str("header", key).Msg("Setting header")
|
tlog.App.Debug().Str("header", key).Msg("Setting header")
|
||||||
c.Header(key, value)
|
c.Header(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
basicPassword := utils.GetSecret(acls.Response.BasicAuth.Password, acls.Response.BasicAuth.PasswordFile)
|
basicPassword := utils.GetSecret(acls.Response.BasicAuth.Password, acls.Response.BasicAuth.PasswordFile)
|
||||||
|
|
||||||
if acls.Response.BasicAuth.Username != "" && basicPassword != "" {
|
if acls.Response.BasicAuth.Username != "" && basicPassword != "" {
|
||||||
log.Debug().Str("username", acls.Response.BasicAuth.Username).Msg("Setting basic auth header")
|
tlog.App.Debug().Str("username", acls.Response.BasicAuth.Username).Msg("Setting basic auth header")
|
||||||
c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(acls.Response.BasicAuth.Username, basicPassword)))
|
c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(acls.Response.BasicAuth.Username, basicPassword)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,20 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/bootstrap"
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder, *service.AuthService) {
|
func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder, *service.AuthService) {
|
||||||
|
tlog.NewSimpleLogger().Init()
|
||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
router := gin.Default()
|
router := gin.Default()
|
||||||
@@ -26,14 +31,16 @@ func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.En
|
|||||||
group := router.Group("/api")
|
group := router.Group("/api")
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// Mock app
|
||||||
|
app := bootstrap.NewBootstrapApp(config.Config{})
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
databaseService := service.NewDatabaseService(service.DatabaseServiceConfig{
|
db, err := app.SetupDatabase(":memory:")
|
||||||
DatabasePath: "/tmp/tinyauth_test.db",
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.NilError(t, databaseService.Init())
|
assert.NilError(t, err)
|
||||||
|
|
||||||
database := databaseService.GetDatabase()
|
// Queries
|
||||||
|
queries := repository.New(db)
|
||||||
|
|
||||||
// Docker
|
// Docker
|
||||||
dockerService := service.NewDockerService()
|
dockerService := service.NewDockerService()
|
||||||
@@ -41,7 +48,7 @@ func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.En
|
|||||||
assert.NilError(t, dockerService.Init())
|
assert.NilError(t, dockerService.Init())
|
||||||
|
|
||||||
// Access controls
|
// Access controls
|
||||||
accessControlsService := service.NewAccessControlsService(dockerService)
|
accessControlsService := service.NewAccessControlsService(dockerService, map[string]config.App{})
|
||||||
|
|
||||||
assert.NilError(t, accessControlsService.Init())
|
assert.NilError(t, accessControlsService.Init())
|
||||||
|
|
||||||
@@ -53,14 +60,15 @@ func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.En
|
|||||||
Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test
|
Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
OauthWhitelist: "",
|
OauthWhitelist: []string{},
|
||||||
SessionExpiry: 3600,
|
SessionExpiry: 3600,
|
||||||
SecureCookie: false,
|
SessionMaxLifetime: 0,
|
||||||
CookieDomain: "localhost",
|
SecureCookie: false,
|
||||||
LoginTimeout: 300,
|
CookieDomain: "localhost",
|
||||||
LoginMaxRetries: 3,
|
LoginTimeout: 300,
|
||||||
SessionCookieName: "tinyauth-session",
|
LoginMaxRetries: 3,
|
||||||
}, dockerService, nil, database)
|
SessionCookieName: "tinyauth-session",
|
||||||
|
}, dockerService, nil, queries)
|
||||||
|
|
||||||
// Controller
|
// Controller
|
||||||
ctrl := controller.NewProxyController(controller.ProxyControllerConfig{
|
ctrl := controller.NewProxyController(controller.ProxyControllerConfig{
|
||||||
@@ -81,6 +89,14 @@ func TestProxyHandler(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, 400, recorder.Code)
|
assert.Equal(t, 400, recorder.Code)
|
||||||
|
|
||||||
|
// Test invalid method for non-envoy proxy
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
req = httptest.NewRequest("POST", "/api/auth/traefik", nil)
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 405, recorder.Code)
|
||||||
|
assert.Equal(t, "GET", recorder.Header().Get("Allow"))
|
||||||
|
|
||||||
// Test logged out user (traefik/caddy)
|
// Test logged out user (traefik/caddy)
|
||||||
recorder = httptest.NewRecorder()
|
recorder = httptest.NewRecorder()
|
||||||
req = httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
req = httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||||
@@ -93,7 +109,7 @@ func TestProxyHandler(t *testing.T) {
|
|||||||
assert.Equal(t, 307, recorder.Code)
|
assert.Equal(t, 307, recorder.Code)
|
||||||
assert.Equal(t, "http://localhost:8080/login?redirect_uri=https%3A%2F%2Fexample.com%2Fsomepath", recorder.Header().Get("Location"))
|
assert.Equal(t, "http://localhost:8080/login?redirect_uri=https%3A%2F%2Fexample.com%2Fsomepath", recorder.Header().Get("Location"))
|
||||||
|
|
||||||
// Test logged out user (envoy)
|
// Test logged out user (envoy - POST method)
|
||||||
recorder = httptest.NewRecorder()
|
recorder = httptest.NewRecorder()
|
||||||
req = httptest.NewRequest("POST", "/api/auth/envoy", nil)
|
req = httptest.NewRequest("POST", "/api/auth/envoy", nil)
|
||||||
req.Header.Set("X-Forwarded-Proto", "https")
|
req.Header.Set("X-Forwarded-Proto", "https")
|
||||||
@@ -105,6 +121,18 @@ func TestProxyHandler(t *testing.T) {
|
|||||||
assert.Equal(t, 307, recorder.Code)
|
assert.Equal(t, 307, recorder.Code)
|
||||||
assert.Equal(t, "http://localhost:8080/login?redirect_uri=https%3A%2F%2Fexample.com%2Fsomepath", recorder.Header().Get("Location"))
|
assert.Equal(t, "http://localhost:8080/login?redirect_uri=https%3A%2F%2Fexample.com%2Fsomepath", recorder.Header().Get("Location"))
|
||||||
|
|
||||||
|
// Test logged out user (envoy - DELETE method)
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
req = httptest.NewRequest("DELETE", "/api/auth/envoy", nil)
|
||||||
|
req.Header.Set("X-Forwarded-Proto", "https")
|
||||||
|
req.Header.Set("X-Forwarded-Host", "example.com")
|
||||||
|
req.Header.Set("X-Forwarded-Uri", "/somepath")
|
||||||
|
req.Header.Set("Accept", "text/html")
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 307, recorder.Code)
|
||||||
|
assert.Equal(t, "http://localhost:8080/login?redirect_uri=https%3A%2F%2Fexample.com%2Fsomepath", recorder.Header().Get("Location"))
|
||||||
|
|
||||||
// Test logged out user (nginx)
|
// Test logged out user (nginx)
|
||||||
recorder = httptest.NewRecorder()
|
recorder = httptest.NewRecorder()
|
||||||
req = httptest.NewRequest("GET", "/api/auth/nginx", nil)
|
req = httptest.NewRequest("GET", "/api/auth/nginx", nil)
|
||||||
@@ -115,11 +143,11 @@ func TestProxyHandler(t *testing.T) {
|
|||||||
// Test logged in user
|
// Test logged in user
|
||||||
c := gin.CreateTestContextOnly(recorder, router)
|
c := gin.CreateTestContextOnly(recorder, router)
|
||||||
|
|
||||||
err := authService.CreateSessionCookie(c, &config.SessionCookie{
|
err := authService.CreateSessionCookie(c, &repository.Session{
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Name: "testuser",
|
Name: "testuser",
|
||||||
Email: "testuser@example.com",
|
Email: "testuser@example.com",
|
||||||
Provider: "username",
|
Provider: "local",
|
||||||
TotpPending: false,
|
TotpPending: false,
|
||||||
OAuthGroups: "",
|
OAuthGroups: "",
|
||||||
})
|
})
|
||||||
@@ -136,7 +164,7 @@ func TestProxyHandler(t *testing.T) {
|
|||||||
Email: "testuser@example.com",
|
Email: "testuser@example.com",
|
||||||
IsLoggedIn: true,
|
IsLoggedIn: true,
|
||||||
OAuth: false,
|
OAuth: false,
|
||||||
Provider: "username",
|
Provider: "local",
|
||||||
TotpPending: false,
|
TotpPending: false,
|
||||||
OAuthGroups: "",
|
OAuthGroups: "",
|
||||||
TotpEnabled: false,
|
TotpEnabled: false,
|
||||||
@@ -164,8 +192,9 @@ func TestProxyHandler(t *testing.T) {
|
|||||||
Name: "testuser",
|
Name: "testuser",
|
||||||
Email: "testuser@example.com",
|
Email: "testuser@example.com",
|
||||||
IsLoggedIn: true,
|
IsLoggedIn: true,
|
||||||
|
IsBasicAuth: true,
|
||||||
OAuth: false,
|
OAuth: false,
|
||||||
Provider: "basic",
|
Provider: "local",
|
||||||
TotpPending: false,
|
TotpPending: false,
|
||||||
OAuthGroups: "",
|
OAuthGroups: "",
|
||||||
TotpEnabled: true,
|
TotpEnabled: true,
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"time"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type LoginRequest struct {
|
type LoginRequest struct {
|
||||||
@@ -52,7 +52,7 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
|
|
||||||
err := c.ShouldBindJSON(&req)
|
err := c.ShouldBindJSON(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to bind JSON")
|
tlog.App.Error().Err(err).Msg("Failed to bind JSON")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, gin.H{
|
||||||
"status": 400,
|
"status": 400,
|
||||||
"message": "Bad Request",
|
"message": "Bad Request",
|
||||||
@@ -60,23 +60,18 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
clientIP := c.ClientIP()
|
tlog.App.Debug().Str("username", req.Username).Msg("Login attempt")
|
||||||
|
|
||||||
rateIdentifier := req.Username
|
isLocked, remaining := controller.auth.IsAccountLocked(req.Username)
|
||||||
|
|
||||||
if rateIdentifier == "" {
|
|
||||||
rateIdentifier = clientIP
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Str("username", req.Username).Str("ip", clientIP).Msg("Login attempt")
|
|
||||||
|
|
||||||
isLocked, remainingTime := controller.auth.IsAccountLocked(rateIdentifier)
|
|
||||||
|
|
||||||
if isLocked {
|
if isLocked {
|
||||||
log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("Account is locked due to too many failed login attempts")
|
tlog.App.Warn().Str("username", req.Username).Msg("Account is locked due to too many failed login attempts")
|
||||||
|
tlog.AuditLoginFailure(c, req.Username, "username", "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{
|
c.JSON(429, gin.H{
|
||||||
"status": 429,
|
"status": 429,
|
||||||
"message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remainingTime),
|
"message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remaining),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -84,8 +79,9 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
userSearch := controller.auth.SearchUser(req.Username)
|
userSearch := controller.auth.SearchUser(req.Username)
|
||||||
|
|
||||||
if userSearch.Type == "unknown" {
|
if userSearch.Type == "unknown" {
|
||||||
log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("User not found")
|
tlog.App.Warn().Str("username", req.Username).Msg("User not found")
|
||||||
controller.auth.RecordLoginAttempt(rateIdentifier, false)
|
controller.auth.RecordLoginAttempt(req.Username, false)
|
||||||
|
tlog.AuditLoginFailure(c, req.Username, "username", "user not found")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
"status": 401,
|
"status": 401,
|
||||||
"message": "Unauthorized",
|
"message": "Unauthorized",
|
||||||
@@ -94,8 +90,9 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !controller.auth.VerifyUser(userSearch, req.Password) {
|
if !controller.auth.VerifyUser(userSearch, req.Password) {
|
||||||
log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("Invalid password")
|
tlog.App.Warn().Str("username", req.Username).Msg("Invalid password")
|
||||||
controller.auth.RecordLoginAttempt(rateIdentifier, false)
|
controller.auth.RecordLoginAttempt(req.Username, false)
|
||||||
|
tlog.AuditLoginFailure(c, req.Username, "username", "invalid password")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
"status": 401,
|
"status": 401,
|
||||||
"message": "Unauthorized",
|
"message": "Unauthorized",
|
||||||
@@ -103,26 +100,27 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Str("username", req.Username).Str("ip", clientIP).Msg("Login successful")
|
tlog.App.Info().Str("username", req.Username).Msg("Login successful")
|
||||||
|
tlog.AuditLoginSuccess(c, req.Username, "username")
|
||||||
|
|
||||||
controller.auth.RecordLoginAttempt(rateIdentifier, true)
|
controller.auth.RecordLoginAttempt(req.Username, true)
|
||||||
|
|
||||||
if userSearch.Type == "local" {
|
if userSearch.Type == "local" {
|
||||||
user := controller.auth.GetLocalUser(userSearch.Username)
|
user := controller.auth.GetLocalUser(userSearch.Username)
|
||||||
|
|
||||||
if user.TotpSecret != "" {
|
if user.TotpSecret != "" {
|
||||||
log.Debug().Str("username", req.Username).Msg("User has TOTP enabled, requiring TOTP verification")
|
tlog.App.Debug().Str("username", req.Username).Msg("User has TOTP enabled, requiring TOTP verification")
|
||||||
|
|
||||||
err := controller.auth.CreateSessionCookie(c, &config.SessionCookie{
|
err := controller.auth.CreateSessionCookie(c, &repository.Session{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Name: utils.Capitalize(req.Username),
|
Name: utils.Capitalize(user.Username),
|
||||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.CookieDomain),
|
Email: utils.CompileUserEmail(user.Username, controller.config.CookieDomain),
|
||||||
Provider: "username",
|
Provider: "local",
|
||||||
TotpPending: true,
|
TotpPending: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to create session cookie")
|
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, gin.H{
|
||||||
"status": 500,
|
"status": 500,
|
||||||
"message": "Internal Server Error",
|
"message": "Internal Server Error",
|
||||||
@@ -139,19 +137,23 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionCookie := config.SessionCookie{
|
sessionCookie := repository.Session{
|
||||||
Username: req.Username,
|
Username: req.Username,
|
||||||
Name: utils.Capitalize(req.Username),
|
Name: utils.Capitalize(req.Username),
|
||||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.CookieDomain),
|
Email: utils.CompileUserEmail(req.Username, controller.config.CookieDomain),
|
||||||
Provider: "username",
|
Provider: "local",
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
if userSearch.Type == "ldap" {
|
||||||
|
sessionCookie.Provider = "ldap"
|
||||||
|
}
|
||||||
|
|
||||||
|
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
||||||
|
|
||||||
err = controller.auth.CreateSessionCookie(c, &sessionCookie)
|
err = controller.auth.CreateSessionCookie(c, &sessionCookie)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to create session cookie")
|
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, gin.H{
|
||||||
"status": 500,
|
"status": 500,
|
||||||
"message": "Internal Server Error",
|
"message": "Internal Server Error",
|
||||||
@@ -166,10 +168,15 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (controller *UserController) logoutHandler(c *gin.Context) {
|
func (controller *UserController) logoutHandler(c *gin.Context) {
|
||||||
log.Debug().Msg("Logout request received")
|
tlog.App.Debug().Msg("Logout request received")
|
||||||
|
|
||||||
controller.auth.DeleteSessionCookie(c)
|
controller.auth.DeleteSessionCookie(c)
|
||||||
|
|
||||||
|
context, err := utils.GetContext(c)
|
||||||
|
if err == nil && context.IsLoggedIn {
|
||||||
|
tlog.AuditLogout(c, context.Username, context.Provider)
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "Logout successful",
|
"message": "Logout successful",
|
||||||
@@ -181,7 +188,7 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
|
|
||||||
err := c.ShouldBindJSON(&req)
|
err := c.ShouldBindJSON(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to bind JSON")
|
tlog.App.Error().Err(err).Msg("Failed to bind JSON")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, gin.H{
|
||||||
"status": 400,
|
"status": 400,
|
||||||
"message": "Bad Request",
|
"message": "Bad Request",
|
||||||
@@ -192,7 +199,7 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
context, err := utils.GetContext(c)
|
context, err := utils.GetContext(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to get user context")
|
tlog.App.Error().Err(err).Msg("Failed to get user context")
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, gin.H{
|
||||||
"status": 500,
|
"status": 500,
|
||||||
"message": "Internal Server Error",
|
"message": "Internal Server Error",
|
||||||
@@ -201,7 +208,7 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !context.TotpPending {
|
if !context.TotpPending {
|
||||||
log.Warn().Msg("TOTP attempt without a pending TOTP session")
|
tlog.App.Warn().Msg("TOTP attempt without a pending TOTP session")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
"status": 401,
|
"status": 401,
|
||||||
"message": "Unauthorized",
|
"message": "Unauthorized",
|
||||||
@@ -209,23 +216,17 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
clientIP := c.ClientIP()
|
tlog.App.Debug().Str("username", context.Username).Msg("TOTP verification attempt")
|
||||||
|
|
||||||
rateIdentifier := context.Username
|
isLocked, remaining := controller.auth.IsAccountLocked(context.Username)
|
||||||
|
|
||||||
if rateIdentifier == "" {
|
|
||||||
rateIdentifier = clientIP
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Str("username", context.Username).Str("ip", clientIP).Msg("TOTP verification attempt")
|
|
||||||
|
|
||||||
isLocked, remainingTime := controller.auth.IsAccountLocked(rateIdentifier)
|
|
||||||
|
|
||||||
if isLocked {
|
if isLocked {
|
||||||
log.Warn().Str("username", context.Username).Str("ip", clientIP).Msg("Account is locked due to too many failed TOTP attempts")
|
tlog.App.Warn().Str("username", context.Username).Msg("Account is locked due to too many failed TOTP attempts")
|
||||||
|
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{
|
c.JSON(429, gin.H{
|
||||||
"status": 429,
|
"status": 429,
|
||||||
"message": fmt.Sprintf("Too many failed TOTP attempts. Try again in %d seconds", remainingTime),
|
"message": fmt.Sprintf("Too many failed TOTP attempts. Try again in %d seconds", remaining),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -235,8 +236,9 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
ok := totp.Validate(req.Code, user.TotpSecret)
|
ok := totp.Validate(req.Code, user.TotpSecret)
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Warn().Str("username", context.Username).Str("ip", clientIP).Msg("Invalid TOTP code")
|
tlog.App.Warn().Str("username", context.Username).Msg("Invalid TOTP code")
|
||||||
controller.auth.RecordLoginAttempt(rateIdentifier, false)
|
controller.auth.RecordLoginAttempt(context.Username, false)
|
||||||
|
tlog.AuditLoginFailure(c, context.Username, "totp", "invalid totp code")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
"status": 401,
|
"status": 401,
|
||||||
"message": "Unauthorized",
|
"message": "Unauthorized",
|
||||||
@@ -244,23 +246,24 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Str("username", context.Username).Str("ip", clientIP).Msg("TOTP verification successful")
|
tlog.App.Info().Str("username", context.Username).Msg("TOTP verification successful")
|
||||||
|
tlog.AuditLoginSuccess(c, context.Username, "totp")
|
||||||
|
|
||||||
controller.auth.RecordLoginAttempt(rateIdentifier, true)
|
controller.auth.RecordLoginAttempt(context.Username, true)
|
||||||
|
|
||||||
sessionCookie := config.SessionCookie{
|
sessionCookie := repository.Session{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Name: utils.Capitalize(user.Username),
|
Name: utils.Capitalize(user.Username),
|
||||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), controller.config.CookieDomain),
|
Email: utils.CompileUserEmail(user.Username, controller.config.CookieDomain),
|
||||||
Provider: "username",
|
Provider: "local",
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
||||||
|
|
||||||
err = controller.auth.CreateSessionCookie(c, &sessionCookie)
|
err = controller.auth.CreateSessionCookie(c, &sessionCookie)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to create session cookie")
|
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, gin.H{
|
||||||
"status": 500,
|
"status": 500,
|
||||||
"message": "Internal Server Error",
|
"message": "Internal Server Error",
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/bootstrap"
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
@@ -21,6 +24,8 @@ var cookieValue string
|
|||||||
var totpSecret = "6WFZXPEZRK5MZHHYAFW4DAOUYQMCASBJ"
|
var totpSecret = "6WFZXPEZRK5MZHHYAFW4DAOUYQMCASBJ"
|
||||||
|
|
||||||
func setupUserController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) {
|
func setupUserController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) {
|
||||||
|
tlog.NewSimpleLogger().Init()
|
||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
router := gin.Default()
|
router := gin.Default()
|
||||||
@@ -34,14 +39,16 @@ func setupUserController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Eng
|
|||||||
group := router.Group("/api")
|
group := router.Group("/api")
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// Mock app
|
||||||
|
app := bootstrap.NewBootstrapApp(config.Config{})
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
databaseService := service.NewDatabaseService(service.DatabaseServiceConfig{
|
db, err := app.SetupDatabase(":memory:")
|
||||||
DatabasePath: "/tmp/tinyauth_test.db",
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.NilError(t, databaseService.Init())
|
assert.NilError(t, err)
|
||||||
|
|
||||||
database := databaseService.GetDatabase()
|
// Queries
|
||||||
|
queries := repository.New(db)
|
||||||
|
|
||||||
// Auth service
|
// Auth service
|
||||||
authService := service.NewAuthService(service.AuthServiceConfig{
|
authService := service.NewAuthService(service.AuthServiceConfig{
|
||||||
@@ -56,14 +63,15 @@ func setupUserController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Eng
|
|||||||
TotpSecret: totpSecret,
|
TotpSecret: totpSecret,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
OauthWhitelist: "",
|
OauthWhitelist: []string{},
|
||||||
SessionExpiry: 3600,
|
SessionExpiry: 3600,
|
||||||
SecureCookie: false,
|
SessionMaxLifetime: 0,
|
||||||
CookieDomain: "localhost",
|
SecureCookie: false,
|
||||||
LoginTimeout: 300,
|
CookieDomain: "localhost",
|
||||||
LoginMaxRetries: 3,
|
LoginTimeout: 300,
|
||||||
SessionCookieName: "tinyauth-session",
|
LoginMaxRetries: 3,
|
||||||
}, nil, nil, database)
|
SessionCookieName: "tinyauth-session",
|
||||||
|
}, nil, nil, queries)
|
||||||
|
|
||||||
// Controller
|
// Controller
|
||||||
ctrl := controller.NewUserController(controller.UserControllerConfig{
|
ctrl := controller.NewUserController(controller.UserControllerConfig{
|
||||||
@@ -196,7 +204,7 @@ func TestTotpHandler(t *testing.T) {
|
|||||||
Email: "totpuser@example.com",
|
Email: "totpuser@example.com",
|
||||||
IsLoggedIn: false,
|
IsLoggedIn: false,
|
||||||
OAuth: false,
|
OAuth: false,
|
||||||
Provider: "username",
|
Provider: "local",
|
||||||
TotpPending: true,
|
TotpPending: true,
|
||||||
OAuthGroups: "",
|
OAuthGroups: "",
|
||||||
TotpEnabled: true,
|
TotpEnabled: true,
|
||||||
@@ -259,7 +267,7 @@ func TestTotpHandler(t *testing.T) {
|
|||||||
Email: "totpuser@example.com",
|
Email: "totpuser@example.com",
|
||||||
IsLoggedIn: false,
|
IsLoggedIn: false,
|
||||||
OAuth: false,
|
OAuth: false,
|
||||||
Provider: "username",
|
Provider: "local",
|
||||||
TotpPending: true,
|
TotpPending: true,
|
||||||
OAuthGroups: "",
|
OAuthGroups: "",
|
||||||
TotpEnabled: true,
|
TotpEnabled: true,
|
||||||
@@ -282,7 +290,7 @@ func TestTotpHandler(t *testing.T) {
|
|||||||
Email: "totpuser@example.com",
|
Email: "totpuser@example.com",
|
||||||
IsLoggedIn: false,
|
IsLoggedIn: false,
|
||||||
OAuth: false,
|
OAuth: false,
|
||||||
Provider: "username",
|
Provider: "local",
|
||||||
TotpPending: false,
|
TotpPending: false,
|
||||||
OAuthGroups: "",
|
OAuthGroups: "",
|
||||||
TotpEnabled: false,
|
TotpEnabled: false,
|
||||||
|
|||||||
85
internal/controller/well_known_controller.go
Normal file
85
internal/controller/well_known_controller.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OpenIDConnectConfiguration struct {
|
||||||
|
Issuer string `json:"issuer"`
|
||||||
|
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
||||||
|
TokenEndpoint string `json:"token_endpoint"`
|
||||||
|
UserinfoEndpoint string `json:"userinfo_endpoint"`
|
||||||
|
JwksUri string `json:"jwks_uri"`
|
||||||
|
ScopesSupported []string `json:"scopes_supported"`
|
||||||
|
ResponseTypesSupported []string `json:"response_types_supported"`
|
||||||
|
GrantTypesSupported []string `json:"grant_types_supported"`
|
||||||
|
SubjectTypesSupported []string `json:"subject_types_supported"`
|
||||||
|
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
|
||||||
|
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
|
||||||
|
ClaimsSupported []string `json:"claims_supported"`
|
||||||
|
ServiceDocumentation string `json:"service_documentation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WellKnownControllerConfig struct{}
|
||||||
|
|
||||||
|
type WellKnownController struct {
|
||||||
|
config WellKnownControllerConfig
|
||||||
|
engine *gin.Engine
|
||||||
|
oidc *service.OIDCService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWellKnownController(config WellKnownControllerConfig, oidc *service.OIDCService, engine *gin.Engine) *WellKnownController {
|
||||||
|
return &WellKnownController{
|
||||||
|
config: config,
|
||||||
|
oidc: oidc,
|
||||||
|
engine: engine,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (controller *WellKnownController) SetupRoutes() {
|
||||||
|
controller.engine.GET("/.well-known/openid-configuration", controller.OpenIDConnectConfiguration)
|
||||||
|
controller.engine.GET("/.well-known/jwks.json", controller.JWKS)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (controller *WellKnownController) OpenIDConnectConfiguration(c *gin.Context) {
|
||||||
|
issuer := controller.oidc.GetIssuer()
|
||||||
|
c.JSON(200, OpenIDConnectConfiguration{
|
||||||
|
Issuer: issuer,
|
||||||
|
AuthorizationEndpoint: fmt.Sprintf("%s/authorize", issuer),
|
||||||
|
TokenEndpoint: fmt.Sprintf("%s/api/oidc/token", issuer),
|
||||||
|
UserinfoEndpoint: fmt.Sprintf("%s/api/oidc/userinfo", issuer),
|
||||||
|
JwksUri: fmt.Sprintf("%s/.well-known/jwks.json", issuer),
|
||||||
|
ScopesSupported: service.SupportedScopes,
|
||||||
|
ResponseTypesSupported: service.SupportedResponseTypes,
|
||||||
|
GrantTypesSupported: service.SupportedGrantTypes,
|
||||||
|
SubjectTypesSupported: []string{"pairwise"},
|
||||||
|
IDTokenSigningAlgValuesSupported: []string{"RS256"},
|
||||||
|
TokenEndpointAuthMethodsSupported: []string{"client_secret_basic", "client_secret_post"},
|
||||||
|
ClaimsSupported: []string{"sub", "updated_at", "name", "preferred_username", "email", "groups"},
|
||||||
|
ServiceDocumentation: "https://tinyauth.app/docs/reference/openid",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (controller *WellKnownController) JWKS(c *gin.Context) {
|
||||||
|
jwks, err := controller.oidc.GetJWK()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, gin.H{
|
||||||
|
"status": "500",
|
||||||
|
"message": "failed to get JWK",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header("content-type", "application/json")
|
||||||
|
|
||||||
|
c.Writer.WriteString(`{"keys":[`)
|
||||||
|
c.Writer.Write(jwks)
|
||||||
|
c.Writer.WriteString(`]}`)
|
||||||
|
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
}
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var OIDCIgnorePaths = []string{"/api/oidc/token", "/api/oidc/userinfo"}
|
||||||
|
|
||||||
type ContextMiddlewareConfig struct {
|
type ContextMiddlewareConfig struct {
|
||||||
CookieDomain string
|
CookieDomain string
|
||||||
}
|
}
|
||||||
@@ -36,10 +39,17 @@ func (m *ContextMiddleware) Init() error {
|
|||||||
|
|
||||||
func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
// There is no point in trying to get credentials if it's an OIDC endpoint
|
||||||
|
path := c.Request.URL.Path
|
||||||
|
if slices.Contains(OIDCIgnorePaths, strings.TrimSuffix(path, "/")) {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cookie, err := m.auth.GetSessionCookie(c)
|
cookie, err := m.auth.GetSessionCookie(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug().Err(err).Msg("No valid session cookie found")
|
tlog.App.Debug().Err(err).Msg("No valid session cookie found")
|
||||||
goto basic
|
goto basic
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +58,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
Username: cookie.Username,
|
Username: cookie.Username,
|
||||||
Name: cookie.Name,
|
Name: cookie.Name,
|
||||||
Email: cookie.Email,
|
Email: cookie.Email,
|
||||||
Provider: "username",
|
Provider: "local",
|
||||||
TotpPending: true,
|
TotpPending: true,
|
||||||
TotpEnabled: true,
|
TotpEnabled: true,
|
||||||
})
|
})
|
||||||
@@ -57,22 +67,44 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch cookie.Provider {
|
switch cookie.Provider {
|
||||||
case "username":
|
case "local", "ldap":
|
||||||
userSearch := m.auth.SearchUser(cookie.Username)
|
userSearch := m.auth.SearchUser(cookie.Username)
|
||||||
|
|
||||||
if userSearch.Type == "unknown" || userSearch.Type == "error" {
|
if userSearch.Type == "unknown" {
|
||||||
log.Debug().Msg("User from session cookie not found")
|
tlog.App.Debug().Msg("User from session cookie not found")
|
||||||
m.auth.DeleteSessionCookie(c)
|
m.auth.DeleteSessionCookie(c)
|
||||||
goto basic
|
goto basic
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if userSearch.Type != cookie.Provider {
|
||||||
|
tlog.App.Warn().Msg("User type from session cookie does not match user search type")
|
||||||
|
m.auth.DeleteSessionCookie(c)
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var ldapGroups []string
|
||||||
|
|
||||||
|
if cookie.Provider == "ldap" {
|
||||||
|
ldapUser, err := m.auth.GetLdapUser(userSearch.Username)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Error().Err(err).Msg("Error retrieving LDAP user details")
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ldapGroups = ldapUser.Groups
|
||||||
|
}
|
||||||
|
|
||||||
m.auth.RefreshSessionCookie(c)
|
m.auth.RefreshSessionCookie(c)
|
||||||
c.Set("context", &config.UserContext{
|
c.Set("context", &config.UserContext{
|
||||||
Username: cookie.Username,
|
Username: cookie.Username,
|
||||||
Name: cookie.Name,
|
Name: cookie.Name,
|
||||||
Email: cookie.Email,
|
Email: cookie.Email,
|
||||||
Provider: "username",
|
Provider: cookie.Provider,
|
||||||
IsLoggedIn: true,
|
IsLoggedIn: true,
|
||||||
|
LdapGroups: strings.Join(ldapGroups, ","),
|
||||||
})
|
})
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
@@ -80,13 +112,13 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
_, exists := m.broker.GetService(cookie.Provider)
|
_, exists := m.broker.GetService(cookie.Provider)
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
log.Debug().Msg("OAuth provider from session cookie not found")
|
tlog.App.Debug().Msg("OAuth provider from session cookie not found")
|
||||||
m.auth.DeleteSessionCookie(c)
|
m.auth.DeleteSessionCookie(c)
|
||||||
goto basic
|
goto basic
|
||||||
}
|
}
|
||||||
|
|
||||||
if !m.auth.IsEmailWhitelisted(cookie.Email) {
|
if !m.auth.IsEmailWhitelisted(cookie.Email) {
|
||||||
log.Debug().Msg("Email from session cookie not whitelisted")
|
tlog.App.Debug().Msg("Email from session cookie not whitelisted")
|
||||||
m.auth.DeleteSessionCookie(c)
|
m.auth.DeleteSessionCookie(c)
|
||||||
goto basic
|
goto basic
|
||||||
}
|
}
|
||||||
@@ -111,7 +143,17 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
basic := m.auth.GetBasicAuth(c)
|
basic := m.auth.GetBasicAuth(c)
|
||||||
|
|
||||||
if basic == nil {
|
if basic == nil {
|
||||||
log.Debug().Msg("No basic auth provided")
|
tlog.App.Debug().Msg("No basic auth provided")
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
locked, remaining := m.auth.IsAccountLocked(basic.Username)
|
||||||
|
|
||||||
|
if locked {
|
||||||
|
tlog.App.Debug().Msgf("Account for user %s is locked for %d seconds, denying auth", basic.Username, remaining)
|
||||||
|
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.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -119,41 +161,57 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
userSearch := m.auth.SearchUser(basic.Username)
|
userSearch := m.auth.SearchUser(basic.Username)
|
||||||
|
|
||||||
if userSearch.Type == "unknown" || userSearch.Type == "error" {
|
if userSearch.Type == "unknown" || userSearch.Type == "error" {
|
||||||
log.Debug().Msg("User from basic auth not found")
|
m.auth.RecordLoginAttempt(basic.Username, false)
|
||||||
|
tlog.App.Debug().Msg("User from basic auth not found")
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !m.auth.VerifyUser(userSearch, basic.Password) {
|
if !m.auth.VerifyUser(userSearch, basic.Password) {
|
||||||
log.Debug().Msg("Invalid password for basic auth user")
|
m.auth.RecordLoginAttempt(basic.Username, false)
|
||||||
|
tlog.App.Debug().Msg("Invalid password for basic auth user")
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.auth.RecordLoginAttempt(basic.Username, true)
|
||||||
|
|
||||||
switch userSearch.Type {
|
switch userSearch.Type {
|
||||||
case "local":
|
case "local":
|
||||||
log.Debug().Msg("Basic auth user is local")
|
tlog.App.Debug().Msg("Basic auth user is local")
|
||||||
|
|
||||||
user := m.auth.GetLocalUser(basic.Username)
|
user := m.auth.GetLocalUser(basic.Username)
|
||||||
|
|
||||||
c.Set("context", &config.UserContext{
|
c.Set("context", &config.UserContext{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Name: utils.Capitalize(user.Username),
|
Name: utils.Capitalize(user.Username),
|
||||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), m.config.CookieDomain),
|
Email: utils.CompileUserEmail(user.Username, m.config.CookieDomain),
|
||||||
Provider: "basic",
|
Provider: "local",
|
||||||
IsLoggedIn: true,
|
IsLoggedIn: true,
|
||||||
TotpEnabled: user.TotpSecret != "",
|
TotpEnabled: user.TotpSecret != "",
|
||||||
|
IsBasicAuth: true,
|
||||||
})
|
})
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
case "ldap":
|
case "ldap":
|
||||||
log.Debug().Msg("Basic auth user is LDAP")
|
tlog.App.Debug().Msg("Basic auth user is LDAP")
|
||||||
|
|
||||||
|
ldapUser, err := m.auth.GetLdapUser(basic.Username)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Debug().Err(err).Msg("Error retrieving LDAP user details")
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.Set("context", &config.UserContext{
|
c.Set("context", &config.UserContext{
|
||||||
Username: basic.Username,
|
Username: basic.Username,
|
||||||
Name: utils.Capitalize(basic.Username),
|
Name: utils.Capitalize(basic.Username),
|
||||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), m.config.CookieDomain),
|
Email: utils.CompileUserEmail(basic.Username, m.config.CookieDomain),
|
||||||
Provider: "basic",
|
Provider: "ldap",
|
||||||
IsLoggedIn: true,
|
IsLoggedIn: true,
|
||||||
|
LdapGroups: strings.Join(ldapUser.Groups, ","),
|
||||||
|
IsBasicAuth: true,
|
||||||
})
|
})
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/assets"
|
"github.com/steveiliop56/tinyauth/internal/assets"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -39,11 +40,10 @@ func (m *UIMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
path := strings.TrimPrefix(c.Request.URL.Path, "/")
|
path := strings.TrimPrefix(c.Request.URL.Path, "/")
|
||||||
|
|
||||||
|
tlog.App.Debug().Str("path", path).Msg("path")
|
||||||
|
|
||||||
switch strings.SplitN(path, "/", 2)[0] {
|
switch strings.SplitN(path, "/", 2)[0] {
|
||||||
case "api":
|
case "api", "resources", ".well-known":
|
||||||
c.Next()
|
|
||||||
return
|
|
||||||
case "resources":
|
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -49,7 +49,7 @@ func (m *ZerologMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
|
|
||||||
latency := time.Since(tStart).String()
|
latency := time.Since(tStart).String()
|
||||||
|
|
||||||
subLogger := log.With().Str("method", method).
|
subLogger := tlog.HTTP.With().Str("method", method).
|
||||||
Str("path", path).
|
Str("path", path).
|
||||||
Str("address", address).
|
Str("address", address).
|
||||||
Str("client_ip", clientIP).
|
Str("client_ip", clientIP).
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
type Session struct {
|
|
||||||
UUID string `gorm:"column:uuid;primaryKey"`
|
|
||||||
Username string `gorm:"column:username"`
|
|
||||||
Email string `gorm:"column:email"`
|
|
||||||
Name string `gorm:"column:name"`
|
|
||||||
Provider string `gorm:"column:provider"`
|
|
||||||
TOTPPending bool `gorm:"column:totp_pending"`
|
|
||||||
OAuthGroups string `gorm:"column:oauth_groups"`
|
|
||||||
Expiry int64 `gorm:"column:expiry"`
|
|
||||||
OAuthName string `gorm:"column:oauth_name"`
|
|
||||||
OAuthSub string `gorm:"column:oauth_sub"`
|
|
||||||
}
|
|
||||||
31
internal/repository/db.go
Normal file
31
internal/repository/db.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DBTX interface {
|
||||||
|
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
|
||||||
|
PrepareContext(context.Context, string) (*sql.Stmt, error)
|
||||||
|
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
|
||||||
|
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db DBTX) *Queries {
|
||||||
|
return &Queries{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Queries struct {
|
||||||
|
db DBTX
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
||||||
|
return &Queries{
|
||||||
|
db: tx,
|
||||||
|
}
|
||||||
|
}
|
||||||
47
internal/repository/models.go
Normal file
47
internal/repository/models.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
|
||||||
|
package repository
|
||||||
|
|
||||||
|
type OidcCode struct {
|
||||||
|
Sub string
|
||||||
|
CodeHash string
|
||||||
|
Scope string
|
||||||
|
RedirectURI string
|
||||||
|
ClientID string
|
||||||
|
ExpiresAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type OidcToken struct {
|
||||||
|
Sub string
|
||||||
|
AccessTokenHash string
|
||||||
|
RefreshTokenHash string
|
||||||
|
Scope string
|
||||||
|
ClientID string
|
||||||
|
TokenExpiresAt int64
|
||||||
|
RefreshTokenExpiresAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type OidcUserinfo struct {
|
||||||
|
Sub string
|
||||||
|
Name string
|
||||||
|
PreferredUsername string
|
||||||
|
Email string
|
||||||
|
Groups string
|
||||||
|
UpdatedAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
UUID string
|
||||||
|
Username string
|
||||||
|
Email string
|
||||||
|
Name string
|
||||||
|
Provider string
|
||||||
|
TotpPending bool
|
||||||
|
OAuthGroups string
|
||||||
|
Expiry int64
|
||||||
|
CreatedAt int64
|
||||||
|
OAuthName string
|
||||||
|
OAuthSub string
|
||||||
|
}
|
||||||
470
internal/repository/oidc_queries.sql.go
Normal file
470
internal/repository/oidc_queries.sql.go
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: oidc_queries.sql
|
||||||
|
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createOidcCode = `-- name: CreateOidcCode :one
|
||||||
|
INSERT INTO "oidc_codes" (
|
||||||
|
"sub",
|
||||||
|
"code_hash",
|
||||||
|
"scope",
|
||||||
|
"redirect_uri",
|
||||||
|
"client_id",
|
||||||
|
"expires_at"
|
||||||
|
) VALUES (
|
||||||
|
?, ?, ?, ?, ?, ?
|
||||||
|
)
|
||||||
|
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateOidcCodeParams struct {
|
||||||
|
Sub string
|
||||||
|
CodeHash string
|
||||||
|
Scope string
|
||||||
|
RedirectURI string
|
||||||
|
ClientID string
|
||||||
|
ExpiresAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateOidcCode(ctx context.Context, arg CreateOidcCodeParams) (OidcCode, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createOidcCode,
|
||||||
|
arg.Sub,
|
||||||
|
arg.CodeHash,
|
||||||
|
arg.Scope,
|
||||||
|
arg.RedirectURI,
|
||||||
|
arg.ClientID,
|
||||||
|
arg.ExpiresAt,
|
||||||
|
)
|
||||||
|
var i OidcCode
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.CodeHash,
|
||||||
|
&i.Scope,
|
||||||
|
&i.RedirectURI,
|
||||||
|
&i.ClientID,
|
||||||
|
&i.ExpiresAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const createOidcToken = `-- name: CreateOidcToken :one
|
||||||
|
INSERT INTO "oidc_tokens" (
|
||||||
|
"sub",
|
||||||
|
"access_token_hash",
|
||||||
|
"refresh_token_hash",
|
||||||
|
"scope",
|
||||||
|
"client_id",
|
||||||
|
"token_expires_at",
|
||||||
|
"refresh_token_expires_at"
|
||||||
|
) VALUES (
|
||||||
|
?, ?, ?, ?, ?, ?, ?
|
||||||
|
)
|
||||||
|
RETURNING sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateOidcTokenParams struct {
|
||||||
|
Sub string
|
||||||
|
AccessTokenHash string
|
||||||
|
RefreshTokenHash string
|
||||||
|
Scope string
|
||||||
|
ClientID string
|
||||||
|
TokenExpiresAt int64
|
||||||
|
RefreshTokenExpiresAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateOidcToken(ctx context.Context, arg CreateOidcTokenParams) (OidcToken, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createOidcToken,
|
||||||
|
arg.Sub,
|
||||||
|
arg.AccessTokenHash,
|
||||||
|
arg.RefreshTokenHash,
|
||||||
|
arg.Scope,
|
||||||
|
arg.ClientID,
|
||||||
|
arg.TokenExpiresAt,
|
||||||
|
arg.RefreshTokenExpiresAt,
|
||||||
|
)
|
||||||
|
var i OidcToken
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.AccessTokenHash,
|
||||||
|
&i.RefreshTokenHash,
|
||||||
|
&i.Scope,
|
||||||
|
&i.ClientID,
|
||||||
|
&i.TokenExpiresAt,
|
||||||
|
&i.RefreshTokenExpiresAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const createOidcUserInfo = `-- name: CreateOidcUserInfo :one
|
||||||
|
INSERT INTO "oidc_userinfo" (
|
||||||
|
"sub",
|
||||||
|
"name",
|
||||||
|
"preferred_username",
|
||||||
|
"email",
|
||||||
|
"groups",
|
||||||
|
"updated_at"
|
||||||
|
) VALUES (
|
||||||
|
?, ?, ?, ?, ?, ?
|
||||||
|
)
|
||||||
|
RETURNING sub, name, preferred_username, email, "groups", updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateOidcUserInfoParams struct {
|
||||||
|
Sub string
|
||||||
|
Name string
|
||||||
|
PreferredUsername string
|
||||||
|
Email string
|
||||||
|
Groups string
|
||||||
|
UpdatedAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateOidcUserInfo(ctx context.Context, arg CreateOidcUserInfoParams) (OidcUserinfo, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createOidcUserInfo,
|
||||||
|
arg.Sub,
|
||||||
|
arg.Name,
|
||||||
|
arg.PreferredUsername,
|
||||||
|
arg.Email,
|
||||||
|
arg.Groups,
|
||||||
|
arg.UpdatedAt,
|
||||||
|
)
|
||||||
|
var i OidcUserinfo
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.Name,
|
||||||
|
&i.PreferredUsername,
|
||||||
|
&i.Email,
|
||||||
|
&i.Groups,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteExpiredOidcCodes = `-- name: DeleteExpiredOidcCodes :many
|
||||||
|
DELETE FROM "oidc_codes"
|
||||||
|
WHERE "expires_at" < ?
|
||||||
|
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteExpiredOidcCodes(ctx context.Context, expiresAt int64) ([]OidcCode, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, deleteExpiredOidcCodes, expiresAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []OidcCode
|
||||||
|
for rows.Next() {
|
||||||
|
var i OidcCode
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.CodeHash,
|
||||||
|
&i.Scope,
|
||||||
|
&i.RedirectURI,
|
||||||
|
&i.ClientID,
|
||||||
|
&i.ExpiresAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteExpiredOidcTokens = `-- name: DeleteExpiredOidcTokens :many
|
||||||
|
DELETE FROM "oidc_tokens"
|
||||||
|
WHERE "token_expires_at" < ? AND "refresh_token_expires_at" < ?
|
||||||
|
RETURNING sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type DeleteExpiredOidcTokensParams struct {
|
||||||
|
TokenExpiresAt int64
|
||||||
|
RefreshTokenExpiresAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) DeleteExpiredOidcTokens(ctx context.Context, arg DeleteExpiredOidcTokensParams) ([]OidcToken, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, deleteExpiredOidcTokens, arg.TokenExpiresAt, arg.RefreshTokenExpiresAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []OidcToken
|
||||||
|
for rows.Next() {
|
||||||
|
var i OidcToken
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.AccessTokenHash,
|
||||||
|
&i.RefreshTokenHash,
|
||||||
|
&i.Scope,
|
||||||
|
&i.ClientID,
|
||||||
|
&i.TokenExpiresAt,
|
||||||
|
&i.RefreshTokenExpiresAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteOidcCode = `-- name: DeleteOidcCode :exec
|
||||||
|
DELETE FROM "oidc_codes"
|
||||||
|
WHERE "code_hash" = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteOidcCode(ctx context.Context, codeHash string) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteOidcCode, codeHash)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteOidcCodeBySub = `-- name: DeleteOidcCodeBySub :exec
|
||||||
|
DELETE FROM "oidc_codes"
|
||||||
|
WHERE "sub" = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteOidcCodeBySub(ctx context.Context, sub string) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteOidcCodeBySub, sub)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteOidcToken = `-- name: DeleteOidcToken :exec
|
||||||
|
DELETE FROM "oidc_tokens"
|
||||||
|
WHERE "access_token_hash" = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteOidcToken(ctx context.Context, accessTokenHash string) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteOidcToken, accessTokenHash)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteOidcTokenBySub = `-- name: DeleteOidcTokenBySub :exec
|
||||||
|
DELETE FROM "oidc_tokens"
|
||||||
|
WHERE "sub" = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteOidcTokenBySub(ctx context.Context, sub string) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteOidcTokenBySub, sub)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteOidcUserInfo = `-- name: DeleteOidcUserInfo :exec
|
||||||
|
DELETE FROM "oidc_userinfo"
|
||||||
|
WHERE "sub" = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteOidcUserInfo(ctx context.Context, sub string) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteOidcUserInfo, sub)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOidcCode = `-- name: GetOidcCode :one
|
||||||
|
DELETE FROM "oidc_codes"
|
||||||
|
WHERE "code_hash" = ?
|
||||||
|
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetOidcCode(ctx context.Context, codeHash string) (OidcCode, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getOidcCode, codeHash)
|
||||||
|
var i OidcCode
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.CodeHash,
|
||||||
|
&i.Scope,
|
||||||
|
&i.RedirectURI,
|
||||||
|
&i.ClientID,
|
||||||
|
&i.ExpiresAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOidcCodeBySub = `-- name: GetOidcCodeBySub :one
|
||||||
|
DELETE FROM "oidc_codes"
|
||||||
|
WHERE "sub" = ?
|
||||||
|
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetOidcCodeBySub(ctx context.Context, sub string) (OidcCode, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getOidcCodeBySub, sub)
|
||||||
|
var i OidcCode
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.CodeHash,
|
||||||
|
&i.Scope,
|
||||||
|
&i.RedirectURI,
|
||||||
|
&i.ClientID,
|
||||||
|
&i.ExpiresAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOidcCodeBySubUnsafe = `-- name: GetOidcCodeBySubUnsafe :one
|
||||||
|
SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at FROM "oidc_codes"
|
||||||
|
WHERE "sub" = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetOidcCodeBySubUnsafe(ctx context.Context, sub string) (OidcCode, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getOidcCodeBySubUnsafe, sub)
|
||||||
|
var i OidcCode
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.CodeHash,
|
||||||
|
&i.Scope,
|
||||||
|
&i.RedirectURI,
|
||||||
|
&i.ClientID,
|
||||||
|
&i.ExpiresAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOidcCodeUnsafe = `-- name: GetOidcCodeUnsafe :one
|
||||||
|
SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at FROM "oidc_codes"
|
||||||
|
WHERE "code_hash" = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetOidcCodeUnsafe(ctx context.Context, codeHash string) (OidcCode, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getOidcCodeUnsafe, codeHash)
|
||||||
|
var i OidcCode
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.CodeHash,
|
||||||
|
&i.Scope,
|
||||||
|
&i.RedirectURI,
|
||||||
|
&i.ClientID,
|
||||||
|
&i.ExpiresAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOidcToken = `-- name: GetOidcToken :one
|
||||||
|
SELECT sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at FROM "oidc_tokens"
|
||||||
|
WHERE "access_token_hash" = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetOidcToken(ctx context.Context, accessTokenHash string) (OidcToken, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getOidcToken, accessTokenHash)
|
||||||
|
var i OidcToken
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.AccessTokenHash,
|
||||||
|
&i.RefreshTokenHash,
|
||||||
|
&i.Scope,
|
||||||
|
&i.ClientID,
|
||||||
|
&i.TokenExpiresAt,
|
||||||
|
&i.RefreshTokenExpiresAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOidcTokenByRefreshToken = `-- name: GetOidcTokenByRefreshToken :one
|
||||||
|
SELECT sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at FROM "oidc_tokens"
|
||||||
|
WHERE "refresh_token_hash" = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetOidcTokenByRefreshToken(ctx context.Context, refreshTokenHash string) (OidcToken, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getOidcTokenByRefreshToken, refreshTokenHash)
|
||||||
|
var i OidcToken
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.AccessTokenHash,
|
||||||
|
&i.RefreshTokenHash,
|
||||||
|
&i.Scope,
|
||||||
|
&i.ClientID,
|
||||||
|
&i.TokenExpiresAt,
|
||||||
|
&i.RefreshTokenExpiresAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOidcTokenBySub = `-- name: GetOidcTokenBySub :one
|
||||||
|
SELECT sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at FROM "oidc_tokens"
|
||||||
|
WHERE "sub" = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetOidcTokenBySub(ctx context.Context, sub string) (OidcToken, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getOidcTokenBySub, sub)
|
||||||
|
var i OidcToken
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.AccessTokenHash,
|
||||||
|
&i.RefreshTokenHash,
|
||||||
|
&i.Scope,
|
||||||
|
&i.ClientID,
|
||||||
|
&i.TokenExpiresAt,
|
||||||
|
&i.RefreshTokenExpiresAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOidcUserInfo = `-- name: GetOidcUserInfo :one
|
||||||
|
SELECT sub, name, preferred_username, email, "groups", updated_at FROM "oidc_userinfo"
|
||||||
|
WHERE "sub" = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetOidcUserInfo(ctx context.Context, sub string) (OidcUserinfo, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getOidcUserInfo, sub)
|
||||||
|
var i OidcUserinfo
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.Name,
|
||||||
|
&i.PreferredUsername,
|
||||||
|
&i.Email,
|
||||||
|
&i.Groups,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateOidcTokenByRefreshToken = `-- name: UpdateOidcTokenByRefreshToken :one
|
||||||
|
UPDATE "oidc_tokens" SET
|
||||||
|
"access_token_hash" = ?,
|
||||||
|
"refresh_token_hash" = ?,
|
||||||
|
"token_expires_at" = ?,
|
||||||
|
"refresh_token_expires_at" = ?
|
||||||
|
WHERE "refresh_token_hash" = ?
|
||||||
|
RETURNING sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateOidcTokenByRefreshTokenParams struct {
|
||||||
|
AccessTokenHash string
|
||||||
|
RefreshTokenHash string
|
||||||
|
TokenExpiresAt int64
|
||||||
|
RefreshTokenExpiresAt int64
|
||||||
|
RefreshTokenHash_2 string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateOidcTokenByRefreshToken(ctx context.Context, arg UpdateOidcTokenByRefreshTokenParams) (OidcToken, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, updateOidcTokenByRefreshToken,
|
||||||
|
arg.AccessTokenHash,
|
||||||
|
arg.RefreshTokenHash,
|
||||||
|
arg.TokenExpiresAt,
|
||||||
|
arg.RefreshTokenExpiresAt,
|
||||||
|
arg.RefreshTokenHash_2,
|
||||||
|
)
|
||||||
|
var i OidcToken
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.AccessTokenHash,
|
||||||
|
&i.RefreshTokenHash,
|
||||||
|
&i.Scope,
|
||||||
|
&i.ClientID,
|
||||||
|
&i.TokenExpiresAt,
|
||||||
|
&i.RefreshTokenExpiresAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
176
internal/repository/session_queries.sql.go
Normal file
176
internal/repository/session_queries.sql.go
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: session_queries.sql
|
||||||
|
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createSession = `-- name: CreateSession :one
|
||||||
|
INSERT INTO "sessions" (
|
||||||
|
"uuid",
|
||||||
|
"username",
|
||||||
|
"email",
|
||||||
|
"name",
|
||||||
|
"provider",
|
||||||
|
"totp_pending",
|
||||||
|
"oauth_groups",
|
||||||
|
"expiry",
|
||||||
|
"created_at",
|
||||||
|
"oauth_name",
|
||||||
|
"oauth_sub"
|
||||||
|
) VALUES (
|
||||||
|
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||||
|
)
|
||||||
|
RETURNING uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, created_at, oauth_name, oauth_sub
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateSessionParams struct {
|
||||||
|
UUID string
|
||||||
|
Username string
|
||||||
|
Email string
|
||||||
|
Name string
|
||||||
|
Provider string
|
||||||
|
TotpPending bool
|
||||||
|
OAuthGroups string
|
||||||
|
Expiry int64
|
||||||
|
CreatedAt int64
|
||||||
|
OAuthName string
|
||||||
|
OAuthSub string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createSession,
|
||||||
|
arg.UUID,
|
||||||
|
arg.Username,
|
||||||
|
arg.Email,
|
||||||
|
arg.Name,
|
||||||
|
arg.Provider,
|
||||||
|
arg.TotpPending,
|
||||||
|
arg.OAuthGroups,
|
||||||
|
arg.Expiry,
|
||||||
|
arg.CreatedAt,
|
||||||
|
arg.OAuthName,
|
||||||
|
arg.OAuthSub,
|
||||||
|
)
|
||||||
|
var i Session
|
||||||
|
err := row.Scan(
|
||||||
|
&i.UUID,
|
||||||
|
&i.Username,
|
||||||
|
&i.Email,
|
||||||
|
&i.Name,
|
||||||
|
&i.Provider,
|
||||||
|
&i.TotpPending,
|
||||||
|
&i.OAuthGroups,
|
||||||
|
&i.Expiry,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.OAuthName,
|
||||||
|
&i.OAuthSub,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteExpiredSessions = `-- name: DeleteExpiredSessions :exec
|
||||||
|
DELETE FROM "sessions"
|
||||||
|
WHERE "expiry" < ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteExpiredSessions(ctx context.Context, expiry int64) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteExpiredSessions, expiry)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteSession = `-- name: DeleteSession :exec
|
||||||
|
DELETE FROM "sessions"
|
||||||
|
WHERE "uuid" = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteSession(ctx context.Context, uuid string) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteSession, uuid)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSession = `-- name: GetSession :one
|
||||||
|
SELECT uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, created_at, oauth_name, oauth_sub FROM "sessions"
|
||||||
|
WHERE "uuid" = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetSession(ctx context.Context, uuid string) (Session, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getSession, uuid)
|
||||||
|
var i Session
|
||||||
|
err := row.Scan(
|
||||||
|
&i.UUID,
|
||||||
|
&i.Username,
|
||||||
|
&i.Email,
|
||||||
|
&i.Name,
|
||||||
|
&i.Provider,
|
||||||
|
&i.TotpPending,
|
||||||
|
&i.OAuthGroups,
|
||||||
|
&i.Expiry,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.OAuthName,
|
||||||
|
&i.OAuthSub,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSession = `-- name: UpdateSession :one
|
||||||
|
UPDATE "sessions" SET
|
||||||
|
"username" = ?,
|
||||||
|
"email" = ?,
|
||||||
|
"name" = ?,
|
||||||
|
"provider" = ?,
|
||||||
|
"totp_pending" = ?,
|
||||||
|
"oauth_groups" = ?,
|
||||||
|
"expiry" = ?,
|
||||||
|
"oauth_name" = ?,
|
||||||
|
"oauth_sub" = ?
|
||||||
|
WHERE "uuid" = ?
|
||||||
|
RETURNING uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, created_at, oauth_name, oauth_sub
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateSessionParams struct {
|
||||||
|
Username string
|
||||||
|
Email string
|
||||||
|
Name string
|
||||||
|
Provider string
|
||||||
|
TotpPending bool
|
||||||
|
OAuthGroups string
|
||||||
|
Expiry int64
|
||||||
|
OAuthName string
|
||||||
|
OAuthSub string
|
||||||
|
UUID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) (Session, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, updateSession,
|
||||||
|
arg.Username,
|
||||||
|
arg.Email,
|
||||||
|
arg.Name,
|
||||||
|
arg.Provider,
|
||||||
|
arg.TotpPending,
|
||||||
|
arg.OAuthGroups,
|
||||||
|
arg.Expiry,
|
||||||
|
arg.OAuthName,
|
||||||
|
arg.OAuthSub,
|
||||||
|
arg.UUID,
|
||||||
|
)
|
||||||
|
var i Session
|
||||||
|
err := row.Scan(
|
||||||
|
&i.UUID,
|
||||||
|
&i.Username,
|
||||||
|
&i.Email,
|
||||||
|
&i.Name,
|
||||||
|
&i.Provider,
|
||||||
|
&i.TotpPending,
|
||||||
|
&i.OAuthGroups,
|
||||||
|
&i.Expiry,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.OAuthName,
|
||||||
|
&i.OAuthSub,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
@@ -1,122 +1,54 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
|
||||||
Environment variable/flag based ACLs are disabled until v5 due to a technical challenge
|
|
||||||
with the current parsing logic.
|
|
||||||
|
|
||||||
The current parser works for simple OAuth provider configs like:
|
|
||||||
- PROVIDERS_MY_AMAZING_PROVIDER_CLIENT_ID
|
|
||||||
|
|
||||||
However, it breaks down when handling nested structs required for ACLs. The custom parsing
|
|
||||||
solution that worked for v4 OAuth providers is incompatible with the ACL parsing logic,
|
|
||||||
making the codebase unmaintainable and fragile.
|
|
||||||
|
|
||||||
A solution is being considered for v5 that would standardize the format to something like:
|
|
||||||
- TINYAUTH_PROVIDERS_GOOGLE_CLIENTSECRET
|
|
||||||
- TINYAUTH_APPS_MYAPP_CONFIG_DOMAIN
|
|
||||||
|
|
||||||
This would allow the Traefik parser to handle everything consistently, but requires a
|
|
||||||
config migration. Until this is resolved, environment-based ACLs are disabled and only
|
|
||||||
Docker label-based ACLs are supported.
|
|
||||||
|
|
||||||
See: https://discord.com/channels/1337450123600465984/1337459086270271538/1434986689935179838 for more information
|
|
||||||
*/
|
|
||||||
|
|
||||||
type AccessControlsService struct {
|
type AccessControlsService struct {
|
||||||
docker *DockerService
|
docker *DockerService
|
||||||
// envACLs config.Apps
|
static map[string]config.App
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAccessControlsService(docker *DockerService) *AccessControlsService {
|
func NewAccessControlsService(docker *DockerService, static map[string]config.App) *AccessControlsService {
|
||||||
return &AccessControlsService{
|
return &AccessControlsService{
|
||||||
docker: docker,
|
docker: docker,
|
||||||
|
static: static,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (acls *AccessControlsService) Init() error {
|
func (acls *AccessControlsService) Init() error {
|
||||||
// acls.envACLs = config.Apps{}
|
return nil // No initialization needed
|
||||||
// env := os.Environ()
|
|
||||||
// appEnvVars := []string{}
|
|
||||||
|
|
||||||
// for _, e := range env {
|
|
||||||
// if strings.HasPrefix(e, "TINYAUTH_APPS_") {
|
|
||||||
// appEnvVars = append(appEnvVars, e)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// err := acls.loadEnvACLs(appEnvVars)
|
|
||||||
|
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return nil
|
|
||||||
|
|
||||||
return nil
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// func (acls *AccessControlsService) loadEnvACLs(appEnvVars []string) error {
|
func (acls *AccessControlsService) lookupStaticACLs(domain string) (config.App, error) {
|
||||||
// if len(appEnvVars) == 0 {
|
for app, config := range acls.static {
|
||||||
// return nil
|
if config.Config.Domain == domain {
|
||||||
// }
|
tlog.App.Debug().Str("name", app).Msg("Found matching container by domain")
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
// envAcls := map[string]string{}
|
if strings.SplitN(domain, ".", 2)[0] == app {
|
||||||
|
tlog.App.Debug().Str("name", app).Msg("Found matching container by app name")
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config.App{}, errors.New("no results")
|
||||||
|
}
|
||||||
|
|
||||||
// for _, e := range appEnvVars {
|
func (acls *AccessControlsService) GetAccessControls(domain string) (config.App, error) {
|
||||||
// parts := strings.SplitN(e, "=", 2)
|
// First check in the static config
|
||||||
// if len(parts) != 2 {
|
app, err := acls.lookupStaticACLs(domain)
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
|
|
||||||
// key := parts[0]
|
if err == nil {
|
||||||
// key = strings.ToLower(key)
|
tlog.App.Debug().Msg("Using ACls from static configuration")
|
||||||
// key = strings.ReplaceAll(key, "_", ".")
|
return app, nil
|
||||||
// value := parts[1]
|
}
|
||||||
// envAcls[key] = value
|
|
||||||
// }
|
|
||||||
|
|
||||||
// apps, err := decoders.DecodeLabels(envAcls)
|
|
||||||
|
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// acls.envACLs = apps
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (acls *AccessControlsService) lookupEnvACLs(appDomain string) *config.App {
|
|
||||||
// if len(acls.envACLs.Apps) == 0 {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
// for appName, appACLs := range acls.envACLs.Apps {
|
|
||||||
// if appACLs.Config.Domain == appDomain {
|
|
||||||
// return &appACLs
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if strings.SplitN(appDomain, ".", 2)[0] == appName {
|
|
||||||
// return &appACLs
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
func (acls *AccessControlsService) GetAccessControls(appDomain string) (config.App, error) {
|
|
||||||
// First check environment variables
|
|
||||||
// envACLs := acls.lookupEnvACLs(appDomain)
|
|
||||||
|
|
||||||
// if envACLs != nil {
|
|
||||||
// log.Debug().Str("domain", appDomain).Msg("Found matching access controls in environment variables")
|
|
||||||
// return *envACLs, nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Fallback to Docker labels
|
// Fallback to Docker labels
|
||||||
return acls.docker.GetLabels(appDomain)
|
tlog.App.Debug().Msg("Falling back to Docker labels for ACLs")
|
||||||
|
return acls.docker.GetLabels(domain)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -9,16 +10,20 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/model"
|
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type LdapGroupsCache struct {
|
||||||
|
Groups []string
|
||||||
|
Expires time.Time
|
||||||
|
}
|
||||||
|
|
||||||
type LoginAttempt struct {
|
type LoginAttempt struct {
|
||||||
FailedAttempts int
|
FailedAttempts int
|
||||||
LastAttempt time.Time
|
LastAttempt time.Time
|
||||||
@@ -26,32 +31,38 @@ type LoginAttempt struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AuthServiceConfig struct {
|
type AuthServiceConfig struct {
|
||||||
Users []config.User
|
Users []config.User
|
||||||
OauthWhitelist string
|
OauthWhitelist []string
|
||||||
SessionExpiry int
|
SessionExpiry int
|
||||||
SecureCookie bool
|
SessionMaxLifetime int
|
||||||
CookieDomain string
|
SecureCookie bool
|
||||||
LoginTimeout int
|
CookieDomain string
|
||||||
LoginMaxRetries int
|
LoginTimeout int
|
||||||
SessionCookieName string
|
LoginMaxRetries int
|
||||||
|
SessionCookieName string
|
||||||
|
IP config.IPConfig
|
||||||
|
LDAPGroupsCacheTTL int
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthService struct {
|
type AuthService struct {
|
||||||
config AuthServiceConfig
|
config AuthServiceConfig
|
||||||
docker *DockerService
|
docker *DockerService
|
||||||
loginAttempts map[string]*LoginAttempt
|
loginAttempts map[string]*LoginAttempt
|
||||||
loginMutex sync.RWMutex
|
ldapGroupsCache map[string]*LdapGroupsCache
|
||||||
ldap *LdapService
|
loginMutex sync.RWMutex
|
||||||
database *gorm.DB
|
ldapGroupsMutex sync.RWMutex
|
||||||
|
ldap *LdapService
|
||||||
|
queries *repository.Queries
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, database *gorm.DB) *AuthService {
|
func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, queries *repository.Queries) *AuthService {
|
||||||
return &AuthService{
|
return &AuthService{
|
||||||
config: config,
|
config: config,
|
||||||
docker: docker,
|
docker: docker,
|
||||||
loginAttempts: make(map[string]*LoginAttempt),
|
loginAttempts: make(map[string]*LoginAttempt),
|
||||||
ldap: ldap,
|
ldapGroupsCache: make(map[string]*LdapGroupsCache),
|
||||||
database: database,
|
ldap: ldap,
|
||||||
|
queries: queries,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,13 +78,13 @@ func (auth *AuthService) SearchUser(username string) config.UserSearch {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if auth.ldap != nil {
|
if auth.ldap.IsConfigured() {
|
||||||
userDN, err := auth.ldap.Search(username)
|
userDN, err := auth.ldap.GetUserDN(username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Str("username", username).Msg("Failed to search for user in LDAP")
|
tlog.App.Warn().Err(err).Str("username", username).Msg("Failed to search for user in LDAP")
|
||||||
return config.UserSearch{
|
return config.UserSearch{
|
||||||
Type: "error",
|
Type: "unknown",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,27 +105,27 @@ func (auth *AuthService) VerifyUser(search config.UserSearch, password string) b
|
|||||||
user := auth.GetLocalUser(search.Username)
|
user := auth.GetLocalUser(search.Username)
|
||||||
return auth.CheckPassword(user, password)
|
return auth.CheckPassword(user, password)
|
||||||
case "ldap":
|
case "ldap":
|
||||||
if auth.ldap != nil {
|
if auth.ldap.IsConfigured() {
|
||||||
err := auth.ldap.Bind(search.Username, password)
|
err := auth.ldap.Bind(search.Username, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Str("username", search.Username).Msg("Failed to bind to LDAP")
|
tlog.App.Warn().Err(err).Str("username", search.Username).Msg("Failed to bind to LDAP")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
err = auth.ldap.Bind(auth.ldap.Config.BindDN, auth.ldap.Config.BindPassword)
|
err = auth.ldap.BindService(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to rebind with service account after user authentication")
|
tlog.App.Error().Err(err).Msg("Failed to rebind with service account after user authentication")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
log.Debug().Str("type", search.Type).Msg("Unknown user type for authentication")
|
tlog.App.Debug().Str("type", search.Type).Msg("Unknown user type for authentication")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Warn().Str("username", search.Username).Msg("User authentication failed")
|
tlog.App.Warn().Str("username", search.Username).Msg("User authentication failed")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,10 +136,45 @@ func (auth *AuthService) GetLocalUser(username string) config.User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Warn().Str("username", username).Msg("Local user not found")
|
tlog.App.Warn().Str("username", username).Msg("Local user not found")
|
||||||
return config.User{}
|
return config.User{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (auth *AuthService) GetLdapUser(userDN string) (config.LdapUser, error) {
|
||||||
|
if !auth.ldap.IsConfigured() {
|
||||||
|
return config.LdapUser{}, errors.New("LDAP service not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.ldapGroupsMutex.RLock()
|
||||||
|
entry, exists := auth.ldapGroupsCache[userDN]
|
||||||
|
auth.ldapGroupsMutex.RUnlock()
|
||||||
|
|
||||||
|
if exists && time.Now().Before(entry.Expires) {
|
||||||
|
return config.LdapUser{
|
||||||
|
DN: userDN,
|
||||||
|
Groups: entry.Groups,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
groups, err := auth.ldap.GetUserGroups(userDN)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return config.LdapUser{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.ldapGroupsMutex.Lock()
|
||||||
|
auth.ldapGroupsCache[userDN] = &LdapGroupsCache{
|
||||||
|
Groups: groups,
|
||||||
|
Expires: time.Now().Add(time.Duration(auth.config.LDAPGroupsCacheTTL) * time.Second),
|
||||||
|
}
|
||||||
|
auth.ldapGroupsMutex.Unlock()
|
||||||
|
|
||||||
|
return config.LdapUser{
|
||||||
|
DN: userDN,
|
||||||
|
Groups: groups,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (auth *AuthService) CheckPassword(user config.User, password string) bool {
|
func (auth *AuthService) CheckPassword(user config.User, password string) bool {
|
||||||
return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil
|
return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil
|
||||||
}
|
}
|
||||||
@@ -180,15 +226,15 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) {
|
|||||||
|
|
||||||
if attempt.FailedAttempts >= auth.config.LoginMaxRetries {
|
if attempt.FailedAttempts >= auth.config.LoginMaxRetries {
|
||||||
attempt.LockedUntil = time.Now().Add(time.Duration(auth.config.LoginTimeout) * time.Second)
|
attempt.LockedUntil = time.Now().Add(time.Duration(auth.config.LoginTimeout) * time.Second)
|
||||||
log.Warn().Str("identifier", identifier).Int("timeout", auth.config.LoginTimeout).Msg("Account locked due to too many failed login attempts")
|
tlog.App.Warn().Str("identifier", identifier).Int("timeout", auth.config.LoginTimeout).Msg("Account locked due to too many failed login attempts")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) IsEmailWhitelisted(email string) bool {
|
func (auth *AuthService) IsEmailWhitelisted(email string) bool {
|
||||||
return utils.CheckFilter(auth.config.OauthWhitelist, email)
|
return utils.CheckFilter(strings.Join(auth.config.OauthWhitelist, ","), email)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.SessionCookie) error {
|
func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *repository.Session) error {
|
||||||
uuid, err := uuid.NewRandom()
|
uuid, err := uuid.NewRandom()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -203,20 +249,21 @@ func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.Sessio
|
|||||||
expiry = auth.config.SessionExpiry
|
expiry = auth.config.SessionExpiry
|
||||||
}
|
}
|
||||||
|
|
||||||
session := model.Session{
|
session := repository.CreateSessionParams{
|
||||||
UUID: uuid.String(),
|
UUID: uuid.String(),
|
||||||
Username: data.Username,
|
Username: data.Username,
|
||||||
Email: data.Email,
|
Email: data.Email,
|
||||||
Name: data.Name,
|
Name: data.Name,
|
||||||
Provider: data.Provider,
|
Provider: data.Provider,
|
||||||
TOTPPending: data.TotpPending,
|
TotpPending: data.TotpPending,
|
||||||
OAuthGroups: data.OAuthGroups,
|
OAuthGroups: data.OAuthGroups,
|
||||||
Expiry: time.Now().Add(time.Duration(expiry) * time.Second).Unix(),
|
Expiry: time.Now().Add(time.Duration(expiry) * time.Second).Unix(),
|
||||||
|
CreatedAt: time.Now().Unix(),
|
||||||
OAuthName: data.OAuthName,
|
OAuthName: data.OAuthName,
|
||||||
OAuthSub: data.OAuthSub,
|
OAuthSub: data.OAuthSub,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = gorm.G[model.Session](auth.database).Create(c, &session)
|
_, err = auth.queries.CreateSession(c, session)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -234,7 +281,7 @@ func (auth *AuthService) RefreshSessionCookie(c *gin.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
session, err := gorm.G[model.Session](auth.database).Where("uuid = ?", cookie).First(c)
|
session, err := auth.queries.GetSession(c, cookie)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -242,21 +289,39 @@ func (auth *AuthService) RefreshSessionCookie(c *gin.Context) error {
|
|||||||
|
|
||||||
currentTime := time.Now().Unix()
|
currentTime := time.Now().Unix()
|
||||||
|
|
||||||
if session.Expiry-currentTime > int64(time.Hour.Seconds()) {
|
var refreshThreshold int64
|
||||||
|
|
||||||
|
if auth.config.SessionExpiry <= int(time.Hour.Seconds()) {
|
||||||
|
refreshThreshold = int64(auth.config.SessionExpiry / 2)
|
||||||
|
} else {
|
||||||
|
refreshThreshold = int64(time.Hour.Seconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.Expiry-currentTime > refreshThreshold {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
newExpiry := currentTime + int64(time.Hour.Seconds())
|
newExpiry := session.Expiry + refreshThreshold
|
||||||
|
|
||||||
_, err = gorm.G[model.Session](auth.database).Where("uuid = ?", cookie).Updates(c, model.Session{
|
_, err = auth.queries.UpdateSession(c, repository.UpdateSessionParams{
|
||||||
Expiry: newExpiry,
|
Username: session.Username,
|
||||||
|
Email: session.Email,
|
||||||
|
Name: session.Name,
|
||||||
|
Provider: session.Provider,
|
||||||
|
TotpPending: session.TotpPending,
|
||||||
|
OAuthGroups: session.OAuthGroups,
|
||||||
|
Expiry: newExpiry,
|
||||||
|
OAuthName: session.OAuthName,
|
||||||
|
OAuthSub: session.OAuthSub,
|
||||||
|
UUID: session.UUID,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie(auth.config.SessionCookieName, cookie, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true)
|
c.SetCookie(auth.config.SessionCookieName, cookie, int(newExpiry-currentTime), "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true)
|
||||||
|
tlog.App.Trace().Str("username", session.Username).Msg("Session cookie refreshed")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -268,7 +333,7 @@ func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = gorm.G[model.Session](auth.database).Where("uuid = ?", cookie).Delete(c)
|
err = auth.queries.DeleteSession(c, cookie)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -279,64 +344,77 @@ func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie, error) {
|
func (auth *AuthService) GetSessionCookie(c *gin.Context) (repository.Session, error) {
|
||||||
cookie, err := c.Cookie(auth.config.SessionCookieName)
|
cookie, err := c.Cookie(auth.config.SessionCookieName)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return config.SessionCookie{}, err
|
return repository.Session{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
session, err := gorm.G[model.Session](auth.database).Where("uuid = ?", cookie).First(c)
|
session, err := auth.queries.GetSession(c, cookie)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return config.SessionCookie{}, err
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
}
|
return repository.Session{}, fmt.Errorf("session not found")
|
||||||
|
}
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
return repository.Session{}, err
|
||||||
return config.SessionCookie{}, fmt.Errorf("session not found")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
currentTime := time.Now().Unix()
|
currentTime := time.Now().Unix()
|
||||||
|
|
||||||
if currentTime > session.Expiry {
|
if auth.config.SessionMaxLifetime != 0 && session.CreatedAt != 0 {
|
||||||
_, err = gorm.G[model.Session](auth.database).Where("uuid = ?", cookie).Delete(c)
|
if currentTime-session.CreatedAt > int64(auth.config.SessionMaxLifetime) {
|
||||||
if err != nil {
|
err = auth.queries.DeleteSession(c, cookie)
|
||||||
log.Error().Err(err).Msg("Failed to delete expired session")
|
if err != nil {
|
||||||
|
tlog.App.Error().Err(err).Msg("Failed to delete session exceeding max lifetime")
|
||||||
|
}
|
||||||
|
return repository.Session{}, fmt.Errorf("session expired due to max lifetime exceeded")
|
||||||
}
|
}
|
||||||
return config.SessionCookie{}, fmt.Errorf("session expired")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return config.SessionCookie{
|
if currentTime > session.Expiry {
|
||||||
|
err = auth.queries.DeleteSession(c, cookie)
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Error().Err(err).Msg("Failed to delete expired session")
|
||||||
|
}
|
||||||
|
return repository.Session{}, fmt.Errorf("session expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
return repository.Session{
|
||||||
UUID: session.UUID,
|
UUID: session.UUID,
|
||||||
Username: session.Username,
|
Username: session.Username,
|
||||||
Email: session.Email,
|
Email: session.Email,
|
||||||
Name: session.Name,
|
Name: session.Name,
|
||||||
Provider: session.Provider,
|
Provider: session.Provider,
|
||||||
TotpPending: session.TOTPPending,
|
TotpPending: session.TotpPending,
|
||||||
OAuthGroups: session.OAuthGroups,
|
OAuthGroups: session.OAuthGroups,
|
||||||
OAuthName: session.OAuthName,
|
OAuthName: session.OAuthName,
|
||||||
OAuthSub: session.OAuthSub,
|
OAuthSub: session.OAuthSub,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) UserAuthConfigured() bool {
|
func (auth *AuthService) LocalAuthConfigured() bool {
|
||||||
return len(auth.config.Users) > 0 || auth.ldap != nil
|
return len(auth.config.Users) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, acls config.App) bool {
|
func (auth *AuthService) LdapAuthConfigured() bool {
|
||||||
|
return auth.ldap.IsConfigured()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *AuthService) IsUserAllowed(c *gin.Context, context config.UserContext, acls config.App) bool {
|
||||||
if context.OAuth {
|
if context.OAuth {
|
||||||
log.Debug().Msg("Checking OAuth whitelist")
|
tlog.App.Debug().Msg("Checking OAuth whitelist")
|
||||||
return utils.CheckFilter(acls.OAuth.Whitelist, context.Email)
|
return utils.CheckFilter(acls.OAuth.Whitelist, context.Email)
|
||||||
}
|
}
|
||||||
|
|
||||||
if acls.Users.Block != "" {
|
if acls.Users.Block != "" {
|
||||||
log.Debug().Msg("Checking blocked users")
|
tlog.App.Debug().Msg("Checking blocked users")
|
||||||
if utils.CheckFilter(acls.Users.Block, context.Username) {
|
if utils.CheckFilter(acls.Users.Block, context.Username) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Checking users")
|
tlog.App.Debug().Msg("Checking users")
|
||||||
return utils.CheckFilter(acls.Users.Allow, context.Username)
|
return utils.CheckFilter(acls.Users.Allow, context.Username)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,19 +425,35 @@ func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserConte
|
|||||||
|
|
||||||
for id := range config.OverrideProviders {
|
for id := range config.OverrideProviders {
|
||||||
if context.Provider == id {
|
if context.Provider == id {
|
||||||
log.Info().Str("provider", id).Msg("OAuth groups not supported for this provider")
|
tlog.App.Info().Str("provider", id).Msg("OAuth groups not supported for this provider")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for userGroup := range strings.SplitSeq(context.OAuthGroups, ",") {
|
for userGroup := range strings.SplitSeq(context.OAuthGroups, ",") {
|
||||||
if utils.CheckFilter(requiredGroups, strings.TrimSpace(userGroup)) {
|
if utils.CheckFilter(requiredGroups, strings.TrimSpace(userGroup)) {
|
||||||
log.Trace().Str("group", userGroup).Str("required", requiredGroups).Msg("User group matched")
|
tlog.App.Trace().Str("group", userGroup).Str("required", requiredGroups).Msg("User group matched")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("No groups matched")
|
tlog.App.Debug().Msg("No groups matched")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *AuthService) IsInLdapGroup(c *gin.Context, context config.UserContext, requiredGroups string) bool {
|
||||||
|
if requiredGroups == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for userGroup := range strings.SplitSeq(context.LdapGroups, ",") {
|
||||||
|
if utils.CheckFilter(requiredGroups, strings.TrimSpace(userGroup)) {
|
||||||
|
tlog.App.Trace().Str("group", userGroup).Str("required", requiredGroups).Msg("User group matched")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tlog.App.Debug().Msg("No groups matched")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,7 +490,7 @@ func (auth *AuthService) IsAuthEnabled(uri string, path config.AppPath) (bool, e
|
|||||||
func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User {
|
func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User {
|
||||||
username, password, ok := c.Request.BasicAuth()
|
username, password, ok := c.Request.BasicAuth()
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debug().Msg("No basic auth provided")
|
tlog.App.Debug().Msg("No basic auth provided")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &config.User{
|
return &config.User{
|
||||||
@@ -406,36 +500,40 @@ func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) CheckIP(acls config.AppIP, ip string) bool {
|
func (auth *AuthService) CheckIP(acls config.AppIP, ip string) bool {
|
||||||
for _, blocked := range acls.Block {
|
// Merge the global and app IP filter
|
||||||
|
blockedIps := append(auth.config.IP.Block, acls.Block...)
|
||||||
|
allowedIPs := append(auth.config.IP.Allow, acls.Allow...)
|
||||||
|
|
||||||
|
for _, blocked := range blockedIps {
|
||||||
res, err := utils.FilterIP(blocked, ip)
|
res, err := utils.FilterIP(blocked, ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list")
|
tlog.App.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if res {
|
if res {
|
||||||
log.Debug().Str("ip", ip).Str("item", blocked).Msg("IP is in blocked list, denying access")
|
tlog.App.Debug().Str("ip", ip).Str("item", blocked).Msg("IP is in blocked list, denying access")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, allowed := range acls.Allow {
|
for _, allowed := range allowedIPs {
|
||||||
res, err := utils.FilterIP(allowed, ip)
|
res, err := utils.FilterIP(allowed, ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list")
|
tlog.App.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if res {
|
if res {
|
||||||
log.Debug().Str("ip", ip).Str("item", allowed).Msg("IP is in allowed list, allowing access")
|
tlog.App.Debug().Str("ip", ip).Str("item", allowed).Msg("IP is in allowed list, allowing access")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(acls.Allow) > 0 {
|
if len(allowedIPs) > 0 {
|
||||||
log.Debug().Str("ip", ip).Msg("IP not in allow list, denying access")
|
tlog.App.Debug().Str("ip", ip).Msg("IP not in allow list, denying access")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Str("ip", ip).Msg("IP not in allow or block list, allowing by default")
|
tlog.App.Debug().Str("ip", ip).Msg("IP not in allow or block list, allowing by default")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,15 +541,15 @@ func (auth *AuthService) IsBypassedIP(acls config.AppIP, ip string) bool {
|
|||||||
for _, bypassed := range acls.Bypass {
|
for _, bypassed := range acls.Bypass {
|
||||||
res, err := utils.FilterIP(bypassed, ip)
|
res, err := utils.FilterIP(bypassed, ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list")
|
tlog.App.Warn().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if res {
|
if res {
|
||||||
log.Debug().Str("ip", ip).Str("item", bypassed).Msg("IP is in bypass list, allowing access")
|
tlog.App.Debug().Str("ip", ip).Str("item", bypassed).Msg("IP is in bypass list, allowing access")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Str("ip", ip).Msg("IP not in bypass list, continuing with authentication")
|
tlog.App.Debug().Str("ip", ip).Msg("IP not in bypass list, continuing with authentication")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/assets"
|
|
||||||
|
|
||||||
"github.com/glebarez/sqlite"
|
|
||||||
"github.com/golang-migrate/migrate/v4"
|
|
||||||
sqliteMigrate "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
|
||||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DatabaseServiceConfig struct {
|
|
||||||
DatabasePath string
|
|
||||||
}
|
|
||||||
|
|
||||||
type DatabaseService struct {
|
|
||||||
config DatabaseServiceConfig
|
|
||||||
database *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDatabaseService(config DatabaseServiceConfig) *DatabaseService {
|
|
||||||
return &DatabaseService{
|
|
||||||
config: config,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ds *DatabaseService) Init() error {
|
|
||||||
dbPath := ds.config.DatabasePath
|
|
||||||
if dbPath == "" {
|
|
||||||
dbPath = "/data/tinyauth.db"
|
|
||||||
}
|
|
||||||
|
|
||||||
dir := filepath.Dir(dbPath)
|
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
||||||
return fmt.Errorf("failed to create database directory %s: %w", dir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
gormDB, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
sqlDB, err := gormDB.DB()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
sqlDB.SetMaxOpenConns(1)
|
|
||||||
|
|
||||||
err = ds.migrateDatabase(sqlDB)
|
|
||||||
|
|
||||||
if err != nil && err != migrate.ErrNoChange {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ds.database = gormDB
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ds *DatabaseService) migrateDatabase(sqlDB *sql.DB) error {
|
|
||||||
data, err := iofs.New(assets.Migrations, "migrations")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
target, err := sqliteMigrate.WithInstance(sqlDB, &sqliteMigrate.Config{})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
migrator, err := migrate.NewWithInstance("iofs", data, "tinyauth", target)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return migrator.Up()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ds *DatabaseService) GetDatabase() *gorm.DB {
|
|
||||||
return ds.database
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user