mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2025-10-28 20:55:42 +00:00
Compare commits
131 Commits
v3.4.0
...
v4.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
866933b3d6 | ||
|
|
d70cbea546 | ||
|
|
50105e4e9d | ||
|
|
51937906ad | ||
|
|
b2dcffdbe4 | ||
|
|
b62b2932fe | ||
|
|
363f0f932f | ||
|
|
9a306f57ec | ||
|
|
039bdb4785 | ||
|
|
5c866bad1a | ||
|
|
2d78e6b598 | ||
|
|
e03eaf4f08 | ||
|
|
74cb8067a8 | ||
|
|
ba46493a7b | ||
|
|
bb0373758a | ||
|
|
f8836fc964 | ||
|
|
53856e0a70 | ||
|
|
9b7dcfd86f | ||
|
|
7afea8b3fc | ||
|
|
f5ac7eff99 | ||
|
|
b024d5ffda | ||
|
|
773cd6d171 | ||
|
|
f3eb7f69b4 | ||
|
|
f0d2da281a | ||
|
|
9ce16c9652 | ||
|
|
ad4fc7ef5f | ||
|
|
5184c96e85 | ||
|
|
b9e35716ac | ||
|
|
17048d94b6 | ||
|
|
55e60a6ed9 | ||
|
|
c7c3de4f78 | ||
|
|
03d06cb0a7 | ||
|
|
87ca77d74c | ||
|
|
504a3b87b4 | ||
|
|
4979121395 | ||
|
|
97020e6e32 | ||
|
|
9f5a02b9f5 | ||
|
|
ef25962a93 | ||
|
|
cc3ce93100 | ||
|
|
b44cef2865 | ||
|
|
fda0f7b3ff | ||
|
|
256f63af05 | ||
|
|
707dcb649d | ||
|
|
351fe1759d | ||
|
|
c968b67af4 | ||
|
|
39f6f5392a | ||
|
|
0102f3146f | ||
|
|
c3a84dad9a | ||
|
|
2fc1260163 | ||
|
|
4dacb46a8e | ||
|
|
5f7f88421e | ||
|
|
bc941cb248 | ||
|
|
00d15de44f | ||
|
|
a4f17de0d1 | ||
|
|
6867667de6 | ||
|
|
079886b54c | ||
|
|
19eb8f3064 | ||
|
|
1a13936693 | ||
|
|
af26d705cd | ||
|
|
2d4ceda12f | ||
|
|
4a87af4463 | ||
|
|
88d918d608 | ||
|
|
5854d973ea | ||
|
|
f25ab72747 | ||
|
|
2233557990 | ||
|
|
d3bec635f8 | ||
|
|
6519644fc1 | ||
|
|
736f65b7b2 | ||
|
|
63d39b5500 | ||
|
|
b735ab6f39 | ||
|
|
232c50eaef | ||
|
|
52b12abeb2 | ||
|
|
48b4d78a7c | ||
|
|
8ebed0ac9a | ||
|
|
e742603c15 | ||
|
|
3215bb6baa | ||
|
|
a11aba72d8 | ||
|
|
10d1b48505 | ||
|
|
f73eb9571f | ||
|
|
da2877a682 | ||
|
|
33cbfef02a | ||
|
|
c1a6428ed3 | ||
|
|
2ee7932cba | ||
|
|
fe440a6f2e | ||
|
|
0ace88a877 | ||
|
|
476ed6964d | ||
|
|
b3dca0429f | ||
|
|
9e4b68112c | ||
|
|
364f0e221e | ||
|
|
09635666aa | ||
|
|
9f02710114 | ||
|
|
64bdab5e5b | ||
|
|
0f4a6b5924 | ||
|
|
c662b9e222 | ||
|
|
a4722db7d7 | ||
|
|
f48bb65d7b | ||
|
|
7e604419ab | ||
|
|
60cd0a216f | ||
|
|
ec6e3aa718 | ||
|
|
6dc57ddf0f | ||
|
|
f780e81ec2 | ||
|
|
8b70ab47a4 | ||
|
|
b800359bb2 | ||
|
|
6ec8c9766c | ||
|
|
4524e3322c | ||
|
|
1941de1125 | ||
|
|
49c4c7a455 | ||
|
|
c10bff55de | ||
|
|
7640e956c2 | ||
|
|
4c18a4c44d | ||
|
|
87a5c0f3f1 | ||
|
|
84d4c84ed2 | ||
|
|
9008b67f7d | ||
|
|
47980a3dd0 | ||
|
|
6cbb9e93c0 | ||
|
|
f3ec4baf3c | ||
|
|
b5799da703 | ||
|
|
80b1820333 | ||
|
|
aed29d2923 | ||
|
|
3397e2aa8e | ||
|
|
ee83c177f4 | ||
|
|
9eb296f146 | ||
|
|
7ac25f0a2a | ||
|
|
6af079d369 | ||
|
|
523267e55b | ||
|
|
e96a2bcaec | ||
|
|
8494fbec8b | ||
|
|
4a2aa6ef43 | ||
|
|
07a5429b6f | ||
|
|
ad4275bee5 | ||
|
|
3665c3a283 |
10
.env.example
10
.env.example
@@ -1,11 +1,9 @@
|
||||
PORT=3000
|
||||
ADDRESS=0.0.0.0
|
||||
SECRET=app_secret
|
||||
SECRET_FILE=app_secret_file
|
||||
APP_URL=http://localhost:3000
|
||||
USERS=your_user_password_hash
|
||||
USERS_FILE=users_file
|
||||
COOKIE_SECURE=false
|
||||
SECURE_COOKIE=false
|
||||
GITHUB_CLIENT_ID=github_client_id
|
||||
GITHUB_CLIENT_SECRET=github_client_secret
|
||||
GITHUB_CLIENT_SECRET_FILE=github_client_secret_file
|
||||
@@ -25,9 +23,11 @@ GENERIC_NAME=My OAuth
|
||||
SESSION_EXPIRY=7200
|
||||
LOGIN_TIMEOUT=300
|
||||
LOGIN_MAX_RETRIES=5
|
||||
LOG_LEVEL=0
|
||||
LOG_LEVEL=debug
|
||||
APP_TITLE=Tinyauth SSO
|
||||
FORGOT_PASSWORD_MESSAGE=Some message about resetting the password
|
||||
OAUTH_AUTO_REDIRECT=none
|
||||
BACKGROUND_IMAGE=some_image_url
|
||||
GENERIC_SKIP_SSL=false
|
||||
GENERIC_SKIP_SSL=false
|
||||
RESOURCES_DIR=/data/resources
|
||||
DATABASE_PATH=/data/tinyauth.db
|
||||
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -39,4 +39,9 @@ jobs:
|
||||
cp -r frontend/dist internal/assets/dist
|
||||
|
||||
- name: Run tests
|
||||
run: go test -v ./...
|
||||
run: go test -coverprofile=coverage.txt -v ./...
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
10
.github/workflows/nightly.yml
vendored
10
.github/workflows/nightly.yml
vendored
@@ -1,6 +1,8 @@
|
||||
name: Nightly Release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
@@ -78,7 +80,9 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
cp -r frontend/dist internal/assets/dist
|
||||
go build -ldflags "-s -w -X tinyauth/internal/constants.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/constants.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/constants.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64
|
||||
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
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -122,7 +126,9 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
cp -r frontend/dist internal/assets/dist
|
||||
go build -ldflags "-s -w -X tinyauth/internal/constants.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/constants.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/constants.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64
|
||||
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
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -58,7 +58,9 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
cp -r frontend/dist internal/assets/dist
|
||||
go build -ldflags "-s -w -X tinyauth/internal/constants.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/constants.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/constants.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64
|
||||
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
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -99,7 +101,9 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
cp -r frontend/dist internal/assets/dist
|
||||
go build -ldflags "-s -w -X tinyauth/internal/constants.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/constants.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/constants.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64
|
||||
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
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -11,11 +11,7 @@ docker-compose.test*
|
||||
users.txt
|
||||
|
||||
# secret test file
|
||||
secret.txt
|
||||
secret_oauth.txt
|
||||
|
||||
# vscode
|
||||
.vscode
|
||||
secret*
|
||||
|
||||
# apple stuff
|
||||
.DS_Store
|
||||
@@ -27,4 +23,7 @@ secret_oauth.txt
|
||||
tmp
|
||||
|
||||
# version files
|
||||
internal/assets/version
|
||||
internal/assets/version
|
||||
|
||||
# data directory
|
||||
data
|
||||
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Connect to server",
|
||||
"type": "go",
|
||||
"request": "attach",
|
||||
"mode": "remote",
|
||||
"remotePath": "/tinyauth",
|
||||
"port": 4000,
|
||||
"host": "127.0.0.1",
|
||||
"debugAdapter": "legacy"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
Dockerfile
10
Dockerfile
@@ -1,5 +1,5 @@
|
||||
# Site builder
|
||||
FROM oven/bun:1.2.15-alpine AS frontend-builder
|
||||
FROM oven/bun:1.2.22-alpine AS frontend-builder
|
||||
|
||||
WORKDIR /frontend
|
||||
|
||||
@@ -20,7 +20,7 @@ COPY ./frontend/vite.config.ts ./
|
||||
RUN bun run build
|
||||
|
||||
# Builder
|
||||
FROM golang:1.24-alpine3.21 AS builder
|
||||
FROM golang:1.25-alpine3.21 AS builder
|
||||
|
||||
ARG VERSION
|
||||
ARG COMMIT_HASH
|
||||
@@ -38,10 +38,10 @@ COPY ./cmd ./cmd
|
||||
COPY ./internal ./internal
|
||||
COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
|
||||
|
||||
RUN go build -ldflags "-s -w -X tinyauth/internal/constants.Version=${VERSION} -X tinyauth/internal/constants.CommitHash=${COMMIT_HASH} -X tinyauth/internal/constants.BuildTimestamp=${BUILD_TIMESTAMP}"
|
||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w -X tinyauth/internal/config.Version=${VERSION} -X tinyauth/internal/config.CommitHash=${COMMIT_HASH} -X tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}"
|
||||
|
||||
# Runner
|
||||
FROM alpine:3.21 AS runner
|
||||
FROM alpine:3.22 AS runner
|
||||
|
||||
WORKDIR /tinyauth
|
||||
|
||||
@@ -51,4 +51,6 @@ COPY --from=builder /tinyauth/tinyauth ./
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
VOLUME ["/data"]
|
||||
|
||||
ENTRYPOINT ["./tinyauth"]
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.24-alpine3.21
|
||||
FROM golang:1.25-alpine3.21
|
||||
|
||||
WORKDIR /tinyauth
|
||||
|
||||
|
||||
25
README.md
25
README.md
@@ -14,39 +14,36 @@
|
||||
|
||||
<br />
|
||||
|
||||
Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic provider to all of your docker apps. It is designed for traefik but it can be extended to work with other reverse proxies like caddy and nginx.
|
||||
Tinyauth is a simple authentication middleware that adds a simple login screen or OAuth with Google, Github and any provider to all of your docker apps. It supports all the popular proxies like Traefik, Nginx and Caddy.
|
||||
|
||||

|
||||
|
||||
> [!WARNING]
|
||||
> Tinyauth is in active development and configuration may change often. Please make sure to carefully read the release notes before updating.
|
||||
|
||||
> [!NOTE]
|
||||
> Tinyauth is intended for homelab use only and it is not made for production use cases. If you are looking for something production ready please use [authentik](https://goauthentik.io) instead.
|
||||
|
||||
## Getting Started
|
||||
|
||||
You can easily get started with tinyauth by following the guide in the [documentation](https://tinyauth.app/docs/getting-started.html). There is also an available [docker compose file](./docker-compose.example.yml) that has traefik, whoami and tinyauth to demonstrate its capabilities.
|
||||
You can easily get started with Tinyauth by following the guide in the [documentation](https://tinyauth.app/docs/getting-started.html). There is also an available [docker compose](./docker-compose.example.yml) file that has Traefik, Whoami and Tinyauth to demonstrate its capabilities.
|
||||
|
||||
## Demo
|
||||
|
||||
If you are still not sure if tinyauth suits your needs you can try out the [demo](https://demo.tinyauth.app). The default username is `user` and the default password is `password`.
|
||||
If you are still not sure if Tinyauth suits your needs you can try out the [demo](https://demo.tinyauth.app). The default username is `user` and the default password is `password`.
|
||||
|
||||
## Documentation
|
||||
|
||||
You can find documentation and guides on all of the available configuration of tinyauth in the [website](https://tinyauth.app).
|
||||
You can find documentation and guides on all of the available configuration of Tinyauth in the [website](https://tinyauth.app).
|
||||
|
||||
## Discord
|
||||
|
||||
I just made a Discord server for tinyauth! It is not only for tinyauth but general self-hosting and homelabbing. [See you there!](https://discord.gg/eHzVaCzRRd).
|
||||
Tinyauth has a [discord](https://discord.gg/eHzVaCzRRd) server. Feel free to hop in to chat about self-hosting, homelabs and of course Tinyauth. See you there!
|
||||
|
||||
## Contributing
|
||||
|
||||
All contributions to the codebase are welcome! If you have any recommendations on how to improve security or find a security issue in tinyauth please open an issue or pull request so it can be fixed as soon as possible!
|
||||
All contributions to the codebase are welcome! If you have any free time feel free to pick up an [issue](https://github.com/steveiliop56/tinyauth/issues) or add your own missing features. Make sure to check out the [contributing guide](./CONTRIBUTING.md) for instructions on how to get the development server up and running.
|
||||
|
||||
## Localization
|
||||
|
||||
If you would like to help translating the project in more languages you can do so by visiting the [Crowdin](https://crowdin.com/project/tinyauth) page.
|
||||
If you would like to help translate Tinyauth into more languages, visit the [Crowdin](https://crowdin.com/project/tinyauth) page.
|
||||
|
||||
## License
|
||||
|
||||
@@ -54,18 +51,16 @@ Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You ma
|
||||
|
||||
## Sponsors
|
||||
|
||||
Thanks a lot 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> <!-- 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> <!-- sponsors -->
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Credits for the logo of this app go to:
|
||||
|
||||
- **Freepik** for providing the police hat and badge.
|
||||
- **Renee French** for the original gopher logo.
|
||||
- **Coderabbit AI** for providing free AI code reviews.
|
||||
- **Syrhu** for providing the bacgkround image of the app.
|
||||
- **Syrhu** for providing the background image of the app.
|
||||
|
||||
## Star History
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Please always use the latest available Tinyauth version which can be found [here](https://github.com/steveiliop56/tinyauth/releases/latest). Older versions (especially major) may contain security issues which I cannot go back and fix.
|
||||
It is recommended to use the [latest](https://github.com/steveiliop56/tinyauth/releases/latest) available version of tinyauth. This is because it includes security fixes, new features and dependency updates. Older versions, especially major ones, are not supported and won't receive security or patch updates.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Due to the nature of this app, it needs to be secure. If you find any security issues in the OAuth or login flow of the app please contact me at <steve@doesmycode.work> and include a concise description of the issue. Please do not use the issues section for reporting major security issues.
|
||||
Due to the nature of this app, it needs to be secure. If you discover any security issues or vulnerabilities in the app please contact me as soon as possible at <steve@doesmycode.work>. Please do not use the issues section to report security issues as I won't be able to patch them in time and they may get exploited by malicious actors.
|
||||
|
||||
6
air.toml
6
air.toml
@@ -2,9 +2,9 @@ root = "/tinyauth"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
pre_cmd = ["mkdir -p internal/assets/dist", "echo 'backend running' > internal/assets/dist/index.html"]
|
||||
cmd = "go build -o ./tmp/tinyauth ."
|
||||
bin = "tmp/tinyauth"
|
||||
pre_cmd = ["mkdir -p internal/assets/dist", "echo 'backend running' > internal/assets/dist/index.html", "go install github.com/go-delve/delve/cmd/dlv@v1.25.0"]
|
||||
cmd = "CGO_ENABLED=0 go build -gcflags=\"all=-N -l\" -o tmp/tinyauth ."
|
||||
bin = "/go/bin/dlv --listen :4000 --headless=true --api-version=2 --accept-multiclient --log=true exec tmp/tinyauth --continue --check-go-version=false"
|
||||
include_ext = ["go"]
|
||||
exclude_dir = ["internal/assets/dist"]
|
||||
exclude_regex = [".*_test\\.go"]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"embeds": [
|
||||
{
|
||||
"title": "Welcome to Tinyauth Discord!",
|
||||
"description": "Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic OAuth provider to all of your docker apps.\n\n**Information**\n\n• Github: <https://github.com/steveiliop56/tinyauth>\n• Website: <https://tinyauth.app>",
|
||||
"description": "Tinyauth is a simple authentication middleware that adds a simple login screen or OAuth with Google, Github and any provider to all of your docker apps. It supports all the popular proxies like Traefik, Nginx and Caddy.\n\n**Information**\n\n• Github: <https://github.com/steveiliop56/tinyauth>\n• Website: <https://tinyauth.app>",
|
||||
"url": "https://tinyauth.app",
|
||||
"color": 7002085,
|
||||
"author": {
|
||||
@@ -12,9 +12,9 @@
|
||||
"footer": {
|
||||
"text": "Updated at"
|
||||
},
|
||||
"timestamp": "2025-03-10T19:00:00.000Z",
|
||||
"timestamp": "2025-06-06T12:25:27.629Z",
|
||||
"thumbnail": {
|
||||
"url": "https://github.com/steveiliop56/tinyauth/blob/main/frontend/public/logo.png?raw=true"
|
||||
"url": "https://github.com/steveiliop56/tinyauth/blob/main/assets/logo.png?raw=true"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
278
cmd/root.go
278
cmd/root.go
@@ -1,21 +1,11 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
totpCmd "tinyauth/cmd/totp"
|
||||
userCmd "tinyauth/cmd/user"
|
||||
"tinyauth/internal/api"
|
||||
"tinyauth/internal/auth"
|
||||
"tinyauth/internal/constants"
|
||||
"tinyauth/internal/docker"
|
||||
"tinyauth/internal/handlers"
|
||||
"tinyauth/internal/hooks"
|
||||
"tinyauth/internal/providers"
|
||||
"tinyauth/internal/types"
|
||||
"tinyauth/internal/bootstrap"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
@@ -30,231 +20,97 @@ var rootCmd = &cobra.Command{
|
||||
Short: "The simplest way to protect your apps with a login screen.",
|
||||
Long: `Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic OAuth provider to all of your docker apps.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Logger
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Logger().Level(zerolog.FatalLevel)
|
||||
var conf config.Config
|
||||
|
||||
// Get config
|
||||
var config types.Config
|
||||
err := viper.Unmarshal(&config)
|
||||
HandleError(err, "Failed to parse config")
|
||||
|
||||
// Secrets
|
||||
config.Secret = utils.GetSecret(config.Secret, config.SecretFile)
|
||||
config.GithubClientSecret = utils.GetSecret(config.GithubClientSecret, config.GithubClientSecretFile)
|
||||
config.GoogleClientSecret = utils.GetSecret(config.GoogleClientSecret, config.GoogleClientSecretFile)
|
||||
config.GenericClientSecret = utils.GetSecret(config.GenericClientSecret, config.GenericClientSecretFile)
|
||||
err := viper.Unmarshal(&conf)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to parse config")
|
||||
}
|
||||
|
||||
// Validate config
|
||||
validator := validator.New()
|
||||
err = validator.Struct(config)
|
||||
HandleError(err, "Failed to validate config")
|
||||
v := validator.New()
|
||||
|
||||
// Logger
|
||||
log.Logger = log.Level(zerolog.Level(config.LogLevel))
|
||||
log.Info().Str("version", strings.TrimSpace(constants.Version)).Msg("Starting tinyauth")
|
||||
|
||||
// Users
|
||||
log.Info().Msg("Parsing users")
|
||||
users, err := utils.GetUsers(config.Users, config.UsersFile)
|
||||
HandleError(err, "Failed to parse users")
|
||||
|
||||
if len(users) == 0 && !utils.OAuthConfigured(config) {
|
||||
HandleError(errors.New("no users or OAuth configured"), "No users or OAuth configured")
|
||||
err = v.Struct(conf)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Invalid config")
|
||||
}
|
||||
|
||||
// Get domain
|
||||
log.Debug().Msg("Getting domain")
|
||||
domain, err := utils.GetUpperDomain(config.AppURL)
|
||||
HandleError(err, "Failed to get upper domain")
|
||||
log.Info().Str("domain", domain).Msg("Using domain for cookie store")
|
||||
log.Logger = log.Level(zerolog.Level(utils.GetLogLevel(conf.LogLevel)))
|
||||
log.Info().Str("version", strings.TrimSpace(config.Version)).Msg("Starting tinyauth")
|
||||
|
||||
// Generate cookie name
|
||||
cookieId := utils.GenerateIdentifier(strings.Split(domain, ".")[0])
|
||||
sessionCookieName := fmt.Sprintf("%s-%s", constants.SessionCookieName, cookieId)
|
||||
csrfCookieName := fmt.Sprintf("%s-%s", constants.CsrfCookieName, cookieId)
|
||||
redirectCookieName := fmt.Sprintf("%s-%s", constants.RedirectCookieName, cookieId)
|
||||
// Create bootstrap app
|
||||
app := bootstrap.NewBootstrapApp(conf)
|
||||
|
||||
// Create OAuth config
|
||||
oauthConfig := types.OAuthConfig{
|
||||
GithubClientId: config.GithubClientId,
|
||||
GithubClientSecret: config.GithubClientSecret,
|
||||
GoogleClientId: config.GoogleClientId,
|
||||
GoogleClientSecret: config.GoogleClientSecret,
|
||||
GenericClientId: config.GenericClientId,
|
||||
GenericClientSecret: config.GenericClientSecret,
|
||||
GenericScopes: strings.Split(config.GenericScopes, ","),
|
||||
GenericAuthURL: config.GenericAuthURL,
|
||||
GenericTokenURL: config.GenericTokenURL,
|
||||
GenericUserURL: config.GenericUserURL,
|
||||
GenericSkipSSL: config.GenericSkipSSL,
|
||||
AppURL: config.AppURL,
|
||||
// Run
|
||||
err = app.Setup()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to setup app")
|
||||
}
|
||||
|
||||
// Create handlers config
|
||||
handlersConfig := types.HandlersConfig{
|
||||
AppURL: config.AppURL,
|
||||
DisableContinue: config.DisableContinue,
|
||||
Title: config.Title,
|
||||
GenericName: config.GenericName,
|
||||
CookieSecure: config.CookieSecure,
|
||||
Domain: domain,
|
||||
ForgotPasswordMessage: config.FogotPasswordMessage,
|
||||
BackgroundImage: config.BackgroundImage,
|
||||
OAuthAutoRedirect: config.OAuthAutoRedirect,
|
||||
CsrfCookieName: csrfCookieName,
|
||||
RedirectCookieName: redirectCookieName,
|
||||
}
|
||||
|
||||
// Create api config
|
||||
apiConfig := types.APIConfig{
|
||||
Port: config.Port,
|
||||
Address: config.Address,
|
||||
}
|
||||
|
||||
// Create auth config
|
||||
authConfig := types.AuthConfig{
|
||||
Users: users,
|
||||
OauthWhitelist: config.OAuthWhitelist,
|
||||
Secret: config.Secret,
|
||||
CookieSecure: config.CookieSecure,
|
||||
SessionExpiry: config.SessionExpiry,
|
||||
Domain: domain,
|
||||
LoginTimeout: config.LoginTimeout,
|
||||
LoginMaxRetries: config.LoginMaxRetries,
|
||||
SessionCookieName: sessionCookieName,
|
||||
}
|
||||
|
||||
// Create hooks config
|
||||
hooksConfig := types.HooksConfig{
|
||||
Domain: domain,
|
||||
}
|
||||
|
||||
// Create docker service
|
||||
docker := docker.NewDocker()
|
||||
|
||||
// Initialize docker
|
||||
err = docker.Init()
|
||||
HandleError(err, "Failed to initialize docker")
|
||||
|
||||
// Create auth service
|
||||
auth := auth.NewAuth(authConfig, docker)
|
||||
|
||||
// Create OAuth providers service
|
||||
providers := providers.NewProviders(oauthConfig)
|
||||
|
||||
// Initialize providers
|
||||
providers.Init()
|
||||
|
||||
// Create hooks service
|
||||
hooks := hooks.NewHooks(hooksConfig, auth, providers)
|
||||
|
||||
// Create handlers
|
||||
handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker)
|
||||
|
||||
// Create API
|
||||
api := api.NewAPI(apiConfig, handlers)
|
||||
|
||||
// Setup routes
|
||||
api.Init()
|
||||
api.SetupRoutes()
|
||||
|
||||
// Start
|
||||
api.Run()
|
||||
},
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
rootCmd.FParseErrWhitelist.UnknownFlags = true
|
||||
err := rootCmd.Execute()
|
||||
HandleError(err, "Failed to execute root command")
|
||||
}
|
||||
|
||||
func HandleError(err error, msg string) {
|
||||
// If error, log it and exit
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg(msg)
|
||||
log.Fatal().Err(err).Msg("Failed to execute command")
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Add user command
|
||||
rootCmd.AddCommand(userCmd.UserCmd())
|
||||
|
||||
// Add totp command
|
||||
rootCmd.AddCommand(totpCmd.TotpCmd())
|
||||
|
||||
// Read environment variables
|
||||
viper.AutomaticEnv()
|
||||
|
||||
// Flags
|
||||
rootCmd.Flags().Int("port", 3000, "Port to run the server on.")
|
||||
rootCmd.Flags().String("address", "0.0.0.0", "Address to bind the server to.")
|
||||
rootCmd.Flags().String("secret", "", "Secret to use for the cookie.")
|
||||
rootCmd.Flags().String("secret-file", "", "Path to a file containing the secret.")
|
||||
rootCmd.Flags().String("app-url", "", "The tinyauth URL.")
|
||||
rootCmd.Flags().String("users", "", "Comma separated list of users in the format username:hash.")
|
||||
rootCmd.Flags().String("users-file", "", "Path to a file containing users in the format username:hash.")
|
||||
rootCmd.Flags().Bool("cookie-secure", false, "Send cookie over secure connection only.")
|
||||
rootCmd.Flags().String("github-client-id", "", "Github OAuth client ID.")
|
||||
rootCmd.Flags().String("github-client-secret", "", "Github OAuth client secret.")
|
||||
rootCmd.Flags().String("github-client-secret-file", "", "Github OAuth client secret file.")
|
||||
rootCmd.Flags().String("google-client-id", "", "Google OAuth client ID.")
|
||||
rootCmd.Flags().String("google-client-secret", "", "Google OAuth client secret.")
|
||||
rootCmd.Flags().String("google-client-secret-file", "", "Google OAuth client secret file.")
|
||||
rootCmd.Flags().String("generic-client-id", "", "Generic OAuth client ID.")
|
||||
rootCmd.Flags().String("generic-client-secret", "", "Generic OAuth client secret.")
|
||||
rootCmd.Flags().String("generic-client-secret-file", "", "Generic OAuth client secret file.")
|
||||
rootCmd.Flags().String("generic-scopes", "", "Generic OAuth scopes.")
|
||||
rootCmd.Flags().String("generic-auth-url", "", "Generic OAuth auth URL.")
|
||||
rootCmd.Flags().String("generic-token-url", "", "Generic OAuth token URL.")
|
||||
rootCmd.Flags().String("generic-user-url", "", "Generic OAuth user info URL.")
|
||||
rootCmd.Flags().String("generic-name", "Generic", "Generic OAuth provider name.")
|
||||
rootCmd.Flags().Bool("generic-skip-ssl", false, "Skip SSL verification for the generic OAuth provider.")
|
||||
rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.")
|
||||
rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.")
|
||||
rootCmd.Flags().String("oauth-auto-redirect", "none", "Auto redirect to the specified OAuth provider if configured. (available providers: github, google, generic)")
|
||||
rootCmd.Flags().Int("session-expiry", 86400, "Session (cookie) expiration time in seconds.")
|
||||
rootCmd.Flags().Int("login-timeout", 300, "Login timeout in seconds after max retries reached (0 to disable).")
|
||||
rootCmd.Flags().Int("login-max-retries", 5, "Maximum login attempts before timeout (0 to disable).")
|
||||
rootCmd.Flags().Int("log-level", 1, "Log level.")
|
||||
rootCmd.Flags().String("app-title", "Tinyauth", "Title of the app.")
|
||||
rootCmd.Flags().String("forgot-password-message", "You can reset your password by changing the `USERS` environment variable.", "Message to show on the forgot password page.")
|
||||
rootCmd.Flags().String("background-image", "/background.jpg", "Background image URL for the login page.")
|
||||
configOptions := []struct {
|
||||
name string
|
||||
defaultVal any
|
||||
description string
|
||||
}{
|
||||
{"port", 3000, "Port to run the server on."},
|
||||
{"address", "0.0.0.0", "Address to bind the server to."},
|
||||
{"app-url", "", "The Tinyauth URL."},
|
||||
{"users", "", "Comma separated list of users in the format username:hash."},
|
||||
{"users-file", "", "Path to a file containing users in the format username:hash."},
|
||||
{"secure-cookie", false, "Send cookie over secure connection only."},
|
||||
{"oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth."},
|
||||
{"oauth-auto-redirect", "none", "Auto redirect to the specified OAuth provider if configured. (available providers: github, google, generic)"},
|
||||
{"session-expiry", 86400, "Session (cookie) expiration time in seconds."},
|
||||
{"login-timeout", 300, "Login timeout in seconds after max retries reached (0 to disable)."},
|
||||
{"login-max-retries", 5, "Maximum login attempts before timeout (0 to disable)."},
|
||||
{"log-level", "info", "Log level."},
|
||||
{"app-title", "Tinyauth", "Title of the app."},
|
||||
{"forgot-password-message", "", "Message to show on the forgot password page."},
|
||||
{"background-image", "/background.jpg", "Background image URL for the login page."},
|
||||
{"ldap-address", "", "LDAP server address (e.g. ldap://localhost:389)."},
|
||||
{"ldap-bind-dn", "", "LDAP bind DN (e.g. uid=user,dc=example,dc=com)."},
|
||||
{"ldap-bind-password", "", "LDAP bind password."},
|
||||
{"ldap-base-dn", "", "LDAP base DN (e.g. dc=example,dc=com)."},
|
||||
{"ldap-insecure", false, "Skip certificate verification for the LDAP server."},
|
||||
{"ldap-search-filter", "(uid=%s)", "LDAP search filter for user lookup."},
|
||||
{"resources-dir", "/data/resources", "Path to a directory containing custom resources (e.g. background image)."},
|
||||
{"database-path", "/data/tinyauth.db", "Path to the Sqlite database file."},
|
||||
{"trusted-proxies", "", "Comma separated list of trusted proxies (IP addresses or CIDRs) for correct client IP detection."},
|
||||
{"disable-analytics", false, "Disable anonymous version collection."},
|
||||
}
|
||||
|
||||
// Bind flags to environment
|
||||
viper.BindEnv("port", "PORT")
|
||||
viper.BindEnv("address", "ADDRESS")
|
||||
viper.BindEnv("secret", "SECRET")
|
||||
viper.BindEnv("secret-file", "SECRET_FILE")
|
||||
viper.BindEnv("app-url", "APP_URL")
|
||||
viper.BindEnv("users", "USERS")
|
||||
viper.BindEnv("users-file", "USERS_FILE")
|
||||
viper.BindEnv("cookie-secure", "COOKIE_SECURE")
|
||||
viper.BindEnv("github-client-id", "GITHUB_CLIENT_ID")
|
||||
viper.BindEnv("github-client-secret", "GITHUB_CLIENT_SECRET")
|
||||
viper.BindEnv("github-client-secret-file", "GITHUB_CLIENT_SECRET_FILE")
|
||||
viper.BindEnv("google-client-id", "GOOGLE_CLIENT_ID")
|
||||
viper.BindEnv("google-client-secret", "GOOGLE_CLIENT_SECRET")
|
||||
viper.BindEnv("google-client-secret-file", "GOOGLE_CLIENT_SECRET_FILE")
|
||||
viper.BindEnv("generic-client-id", "GENERIC_CLIENT_ID")
|
||||
viper.BindEnv("generic-client-secret", "GENERIC_CLIENT_SECRET")
|
||||
viper.BindEnv("generic-client-secret-file", "GENERIC_CLIENT_SECRET_FILE")
|
||||
viper.BindEnv("generic-scopes", "GENERIC_SCOPES")
|
||||
viper.BindEnv("generic-auth-url", "GENERIC_AUTH_URL")
|
||||
viper.BindEnv("generic-token-url", "GENERIC_TOKEN_URL")
|
||||
viper.BindEnv("generic-user-url", "GENERIC_USER_URL")
|
||||
viper.BindEnv("generic-name", "GENERIC_NAME")
|
||||
viper.BindEnv("generic-skip-ssl", "GENERIC_SKIP_SSL")
|
||||
viper.BindEnv("disable-continue", "DISABLE_CONTINUE")
|
||||
viper.BindEnv("oauth-whitelist", "OAUTH_WHITELIST")
|
||||
viper.BindEnv("oauth-auto-redirect", "OAUTH_AUTO_REDIRECT")
|
||||
viper.BindEnv("session-expiry", "SESSION_EXPIRY")
|
||||
viper.BindEnv("log-level", "LOG_LEVEL")
|
||||
viper.BindEnv("app-title", "APP_TITLE")
|
||||
viper.BindEnv("login-timeout", "LOGIN_TIMEOUT")
|
||||
viper.BindEnv("login-max-retries", "LOGIN_MAX_RETRIES")
|
||||
viper.BindEnv("forgot-password-message", "FORGOT_PASSWORD_MESSAGE")
|
||||
viper.BindEnv("background-image", "BACKGROUND_IMAGE")
|
||||
for _, opt := range configOptions {
|
||||
switch v := opt.defaultVal.(type) {
|
||||
case bool:
|
||||
rootCmd.Flags().Bool(opt.name, v, opt.description)
|
||||
case int:
|
||||
rootCmd.Flags().Int(opt.name, v, opt.description)
|
||||
case string:
|
||||
rootCmd.Flags().String(opt.name, v, opt.description)
|
||||
}
|
||||
|
||||
// Create uppercase env var name
|
||||
envVar := strings.ReplaceAll(strings.ToUpper(opt.name), "-", "_")
|
||||
viper.BindEnv(opt.name, envVar)
|
||||
}
|
||||
|
||||
// Bind flags to viper
|
||||
viper.BindPFlags(rootCmd.Flags())
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Interactive flag
|
||||
var interactive bool
|
||||
|
||||
// Input user
|
||||
@@ -25,15 +24,9 @@ var GenerateCmd = &cobra.Command{
|
||||
Use: "generate",
|
||||
Short: "Generate a totp secret",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Setup logger
|
||||
log.Logger = log.Level(zerolog.InfoLevel)
|
||||
|
||||
// Use simple theme
|
||||
var baseTheme *huh.Theme = huh.ThemeBase()
|
||||
|
||||
// Interactive
|
||||
if interactive {
|
||||
// Create huh form
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().Title("Current username:hash").Value(&iUser).Validate((func(s string) error {
|
||||
@@ -44,51 +37,39 @@ var GenerateCmd = &cobra.Command{
|
||||
})),
|
||||
),
|
||||
)
|
||||
|
||||
// Run form
|
||||
var baseTheme *huh.Theme = huh.ThemeBase()
|
||||
err := form.WithTheme(baseTheme).Run()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Form failed")
|
||||
}
|
||||
}
|
||||
|
||||
// Parse user
|
||||
user, err := utils.ParseUser(iUser)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to parse user")
|
||||
}
|
||||
|
||||
// Check if user was using docker escape
|
||||
dockerEscape := false
|
||||
|
||||
if strings.Contains(iUser, "$$") {
|
||||
dockerEscape = true
|
||||
}
|
||||
|
||||
// Check it has totp
|
||||
if user.TotpSecret != "" {
|
||||
log.Fatal().Msg("User already has a totp secret")
|
||||
}
|
||||
|
||||
// Generate totp secret
|
||||
key, err := totp.Generate(totp.GenerateOpts{
|
||||
Issuer: "Tinyauth",
|
||||
AccountName: user.Username,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to generate totp secret")
|
||||
}
|
||||
|
||||
// Create secret
|
||||
secret := key.Secret()
|
||||
|
||||
// Print secret and image
|
||||
log.Info().Str("secret", secret).Msg("Generated totp secret")
|
||||
|
||||
// Print QR code
|
||||
log.Info().Msg("Generated QR code")
|
||||
|
||||
config := qrterminal.Config{
|
||||
@@ -101,7 +82,6 @@ var GenerateCmd = &cobra.Command{
|
||||
|
||||
qrterminal.GenerateWithConfig(key.URL(), config)
|
||||
|
||||
// Add the secret to the user
|
||||
user.TotpSecret = secret
|
||||
|
||||
// If using docker escape re-escape it
|
||||
@@ -109,13 +89,11 @@ var GenerateCmd = &cobra.Command{
|
||||
user.Password = strings.ReplaceAll(user.Password, "$", "$$")
|
||||
}
|
||||
|
||||
// Print success
|
||||
log.Info().Str("user", fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TotpSecret)).Msg("Add the totp secret to your authenticator app then use the verify command to ensure everything is working correctly.")
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Add interactive flag
|
||||
GenerateCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Run in interactive mode")
|
||||
GenerateCmd.Flags().StringVar(&iUser, "user", "", "Your current username:hash")
|
||||
}
|
||||
|
||||
@@ -7,16 +7,11 @@ import (
|
||||
)
|
||||
|
||||
func TotpCmd() *cobra.Command {
|
||||
// Create the totp command
|
||||
totpCmd := &cobra.Command{
|
||||
Use: "totp",
|
||||
Short: "Totp utilities",
|
||||
Long: `Utilities for creating and verifying totp codes.`,
|
||||
}
|
||||
|
||||
// Add the generate command
|
||||
totpCmd.AddCommand(generate.GenerateCmd)
|
||||
|
||||
// Return the totp command
|
||||
return totpCmd
|
||||
}
|
||||
|
||||
@@ -12,10 +12,7 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Interactive flag
|
||||
var interactive bool
|
||||
|
||||
// Docker flag
|
||||
var docker bool
|
||||
|
||||
// i stands for input
|
||||
@@ -27,12 +24,9 @@ var CreateCmd = &cobra.Command{
|
||||
Short: "Create a user",
|
||||
Long: `Create a user either interactively or by passing flags.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Setup logger
|
||||
log.Logger = log.Level(zerolog.InfoLevel)
|
||||
|
||||
// Check if interactive
|
||||
if interactive {
|
||||
// Create huh form
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().Title("Username").Value(&iUsername).Validate((func(s string) error {
|
||||
@@ -50,46 +44,35 @@ var CreateCmd = &cobra.Command{
|
||||
huh.NewSelect[bool]().Title("Format the output for docker?").Options(huh.NewOption("Yes", true), huh.NewOption("No", false)).Value(&docker),
|
||||
),
|
||||
)
|
||||
|
||||
// Use simple theme
|
||||
var baseTheme *huh.Theme = huh.ThemeBase()
|
||||
|
||||
err := form.WithTheme(baseTheme).Run()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Form failed")
|
||||
}
|
||||
}
|
||||
|
||||
// Do we have username and password?
|
||||
if iUsername == "" || iPassword == "" {
|
||||
log.Fatal().Err(errors.New("error invalid input")).Msg("Username and password cannot be empty")
|
||||
}
|
||||
|
||||
log.Info().Str("username", iUsername).Str("password", iPassword).Bool("docker", docker).Msg("Creating user")
|
||||
|
||||
// Hash password
|
||||
password, err := bcrypt.GenerateFromPassword([]byte(iPassword), bcrypt.DefaultCost)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to hash password")
|
||||
}
|
||||
|
||||
// Convert password to string
|
||||
// If docker format is enabled, escape the dollar sign
|
||||
passwordString := string(password)
|
||||
|
||||
// Escape $ for docker
|
||||
if docker {
|
||||
passwordString = strings.ReplaceAll(passwordString, "$", "$$")
|
||||
}
|
||||
|
||||
// Log user created
|
||||
log.Info().Str("user", fmt.Sprintf("%s:%s", iUsername, passwordString)).Msg("User created")
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Flags
|
||||
CreateCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Create a user interactively")
|
||||
CreateCmd.Flags().BoolVar(&docker, "docker", false, "Format output for docker")
|
||||
CreateCmd.Flags().StringVar(&iUsername, "username", "", "Username")
|
||||
|
||||
@@ -8,17 +8,12 @@ import (
|
||||
)
|
||||
|
||||
func UserCmd() *cobra.Command {
|
||||
// Create the user command
|
||||
userCmd := &cobra.Command{
|
||||
Use: "user",
|
||||
Short: "User utilities",
|
||||
Long: `Utilities for creating and verifying tinyauth compatible users.`,
|
||||
}
|
||||
|
||||
// Add subcommands
|
||||
userCmd.AddCommand(create.CreateCmd)
|
||||
userCmd.AddCommand(verify.VerifyCmd)
|
||||
|
||||
// Return the user command
|
||||
return userCmd
|
||||
}
|
||||
|
||||
@@ -12,10 +12,7 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Interactive flag
|
||||
var interactive bool
|
||||
|
||||
// Docker flag
|
||||
var docker bool
|
||||
|
||||
// i stands for input
|
||||
@@ -29,15 +26,9 @@ var VerifyCmd = &cobra.Command{
|
||||
Short: "Verify a user is set up correctly",
|
||||
Long: `Verify a user is set up correctly meaning that it has a correct username, password and totp code.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Setup logger
|
||||
log.Logger = log.Level(zerolog.InfoLevel)
|
||||
|
||||
// Use simple theme
|
||||
var baseTheme *huh.Theme = huh.ThemeBase()
|
||||
|
||||
// Check if interactive
|
||||
if interactive {
|
||||
// Create huh form
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().Title("User (username:hash:totp)").Value(&iUser).Validate((func(s string) error {
|
||||
@@ -61,35 +52,27 @@ var VerifyCmd = &cobra.Command{
|
||||
huh.NewInput().Title("Totp Code (if setup)").Value(&iTotp),
|
||||
),
|
||||
)
|
||||
|
||||
// Run form
|
||||
var baseTheme *huh.Theme = huh.ThemeBase()
|
||||
err := form.WithTheme(baseTheme).Run()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Form failed")
|
||||
}
|
||||
}
|
||||
|
||||
// Parse user
|
||||
user, err := utils.ParseUser(iUser)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to parse user")
|
||||
}
|
||||
|
||||
// Compare username
|
||||
if user.Username != iUsername {
|
||||
log.Fatal().Msg("Username is incorrect")
|
||||
}
|
||||
|
||||
// Compare password
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(iPassword))
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Msg("Ppassword is incorrect")
|
||||
}
|
||||
|
||||
// Check if user has 2fa code
|
||||
if user.TotpSecret == "" {
|
||||
if iTotp != "" {
|
||||
log.Warn().Msg("User does not have 2fa secret")
|
||||
@@ -98,21 +81,17 @@ var VerifyCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
// Check totp code
|
||||
ok := totp.Validate(iTotp, user.TotpSecret)
|
||||
|
||||
if !ok {
|
||||
log.Fatal().Msg("Totp code incorrect")
|
||||
|
||||
}
|
||||
|
||||
// Done
|
||||
log.Info().Msg("User verified")
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Flags
|
||||
VerifyCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Create a user interactively")
|
||||
VerifyCmd.Flags().BoolVar(&docker, "docker", false, "Is the user formatted for docker?")
|
||||
VerifyCmd.Flags().StringVar(&iUsername, "username", "", "Username")
|
||||
|
||||
@@ -2,20 +2,19 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"tinyauth/internal/constants"
|
||||
"tinyauth/internal/config"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Create the version command
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the version number of Tinyauth",
|
||||
Long: `All software has versions. This is Tinyauth's`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("Version: %s\n", constants.Version)
|
||||
fmt.Printf("Commit Hash: %s\n", constants.CommitHash)
|
||||
fmt.Printf("Build Timestamp: %s\n", constants.BuildTimestamp)
|
||||
fmt.Printf("Version: %s\n", config.Version)
|
||||
fmt.Printf("Commit Hash: %s\n", config.CommitHash)
|
||||
fmt.Printf("Build Timestamp: %s\n", config.BuildTimestamp)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
8
codecov.yml
Normal file
8
codecov.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
informational: true
|
||||
patch:
|
||||
default:
|
||||
informational: true
|
||||
@@ -13,8 +13,8 @@ services:
|
||||
image: traefik/whoami:latest
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.nginx.rule: Host(`whoami.example.com`)
|
||||
traefik.http.routers.nginx.middlewares: tinyauth
|
||||
traefik.http.routers.whoami.rule: Host(`whoami.example.com`)
|
||||
traefik.http.routers.whoami.middlewares: tinyauth
|
||||
|
||||
tinyauth-frontend:
|
||||
container_name: tinyauth-frontend
|
||||
@@ -34,14 +34,20 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.dev
|
||||
args:
|
||||
- VERSION=development
|
||||
- COMMIT_HASH=development
|
||||
- BUILD_TIMESTAMP=000-00-00T00:00:00Z
|
||||
env_file: .env
|
||||
volumes:
|
||||
- ./internal:/tinyauth/internal
|
||||
- ./cmd:/tinyauth/cmd
|
||||
- ./main.go:/tinyauth/main.go
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./data:/data
|
||||
ports:
|
||||
- 3000:3000
|
||||
- 4000:4000
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.middlewares.tinyauth.forwardauth.address: http://tinyauth-backend:3000/api/auth/traefik
|
||||
|
||||
@@ -13,16 +13,17 @@ services:
|
||||
image: traefik/whoami:latest
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.nginx.rule: Host(`whoami.example.com`)
|
||||
traefik.http.routers.nginx.middlewares: tinyauth
|
||||
traefik.http.routers.whoami.rule: Host(`whoami.example.com`)
|
||||
traefik.http.routers.whoami.middlewares: tinyauth
|
||||
|
||||
tinyauth:
|
||||
container_name: tinyauth
|
||||
image: ghcr.io/steveiliop56/tinyauth:v3
|
||||
environment:
|
||||
- SECRET=some-random-32-chars-string
|
||||
- APP_URL=https://tinyauth.example.com
|
||||
- USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password
|
||||
volumes:
|
||||
- ./data:/data
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.tinyauth.rule: Host(`tinyauth.example.com`)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM oven/bun:1.1.45-alpine
|
||||
FROM oven/bun:1.2.16-alpine
|
||||
|
||||
WORKDIR /frontend
|
||||
|
||||
|
||||
@@ -4,69 +4,68 @@
|
||||
"": {
|
||||
"name": "tinyauth-shadcn",
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@tailwindcss/vite": "^4.1.8",
|
||||
"@tanstack/react-query": "^5.79.0",
|
||||
"axios": "^1.9.0",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@tanstack/react-query": "^5.89.0",
|
||||
"axios": "^1.12.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dompurify": "^3.2.6",
|
||||
"i18next": "^25.2.1",
|
||||
"i18next-browser-languagedetector": "^8.0.5",
|
||||
"i18next": "^25.5.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.511.0",
|
||||
"lucide-react": "^0.544.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.56.4",
|
||||
"react-i18next": "^15.5.2",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-i18next": "^15.7.3",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router": "^7.6.1",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"zod": "^3.25.42",
|
||||
"react-router": "^7.9.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"zod": "^4.1.9",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.27.0",
|
||||
"@tanstack/eslint-plugin-query": "^5.78.0",
|
||||
"@types/node": "^22.15.27",
|
||||
"@types/react": "^19.1.6",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@vitejs/plugin-react": "^4.5.0",
|
||||
"eslint": "^9.27.0",
|
||||
"@eslint/js": "^9.35.0",
|
||||
"@tanstack/eslint-plugin-query": "^5.89.0",
|
||||
"@types/node": "^24.5.2",
|
||||
"@types/react": "^19.1.13",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@vitejs/plugin-react": "^5.0.3",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.2.0",
|
||||
"prettier": "3.5.3",
|
||||
"tw-animate-css": "^1.3.2",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.33.0",
|
||||
"vite": "^6.3.1",
|
||||
"globals": "^16.4.0",
|
||||
"prettier": "3.6.2",
|
||||
"tw-animate-css": "^1.3.8",
|
||||
"typescript": "~5.9.2",
|
||||
"typescript-eslint": "^8.44.0",
|
||||
"vite": "^7.1.6",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||
|
||||
"@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/compat-data": ["@babel/compat-data@7.27.2", "", {}, "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ=="],
|
||||
|
||||
"@babel/core": ["@babel/core@7.27.1", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.1", "@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-module-transforms": "^7.27.1", "@babel/helpers": "^7.27.1", "@babel/parser": "^7.27.1", "@babel/template": "^7.27.1", "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1", "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-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ=="],
|
||||
"@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=="],
|
||||
|
||||
"@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/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-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-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-transforms": ["@babel/helper-module-transforms@7.27.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g=="],
|
||||
"@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-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
|
||||
|
||||
@@ -76,21 +75,21 @@
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.27.1", "", { "dependencies": { "@babel/template": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ=="],
|
||||
"@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
||||
|
||||
"@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/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.27.1", "", {}, "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog=="],
|
||||
"@babel/runtime": ["@babel/runtime@7.27.6", "", {}, "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q=="],
|
||||
|
||||
"@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/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/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=="],
|
||||
|
||||
"@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/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=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
|
||||
|
||||
@@ -142,23 +141,23 @@
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="],
|
||||
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
|
||||
|
||||
"@eslint/config-array": ["@eslint/config-array@0.20.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ=="],
|
||||
"@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="],
|
||||
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.2.2", "", {}, "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg=="],
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.3.1", "", {}, "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA=="],
|
||||
|
||||
"@eslint/core": ["@eslint/core@0.14.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg=="],
|
||||
"@eslint/core": ["@eslint/core@0.15.2", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg=="],
|
||||
|
||||
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
|
||||
|
||||
"@eslint/js": ["@eslint/js@9.27.0", "", {}, "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA=="],
|
||||
"@eslint/js": ["@eslint/js@9.35.0", "", {}, "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw=="],
|
||||
|
||||
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
|
||||
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.1", "", { "dependencies": { "@eslint/core": "^0.14.0", "levn": "^0.4.1" } }, "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w=="],
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.5", "", { "dependencies": { "@eslint/core": "^0.15.2", "levn": "^0.4.1" } }, "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w=="],
|
||||
|
||||
"@floating-ui/core": ["@floating-ui/core@1.7.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA=="],
|
||||
|
||||
@@ -168,7 +167,7 @@
|
||||
|
||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="],
|
||||
|
||||
"@hookform/resolvers": ["@hookform/resolvers@5.0.1", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA=="],
|
||||
"@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||
|
||||
@@ -180,15 +179,17 @@
|
||||
|
||||
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
|
||||
|
||||
"@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=="],
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@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=="],
|
||||
"@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=="],
|
||||
|
||||
@@ -198,7 +199,7 @@
|
||||
|
||||
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
||||
|
||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="],
|
||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
||||
|
||||
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
|
||||
|
||||
@@ -210,9 +211,9 @@
|
||||
|
||||
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
|
||||
|
||||
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ=="],
|
||||
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
|
||||
|
||||
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA=="],
|
||||
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
|
||||
|
||||
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
|
||||
|
||||
@@ -220,13 +221,13 @@
|
||||
|
||||
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
|
||||
|
||||
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.7", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ=="],
|
||||
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
|
||||
|
||||
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
|
||||
|
||||
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.5", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA=="],
|
||||
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
|
||||
|
||||
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
|
||||
|
||||
@@ -252,85 +253,85 @@
|
||||
|
||||
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.9", "", {}, "sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w=="],
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.35", "", {}, "sha512-slYrCpoxJUqzFDDNlvrOYRazQUNRvWPjXA17dAOISY3rDMxX6k8K4cj2H+hEYMHF81HO3uNd5rHVigAWRM5dSg=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.40.2", "", { "os": "android", "cpu": "arm" }, "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg=="],
|
||||
"@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-arm64": ["@rollup/rollup-android-arm64@4.40.2", "", { "os": "android", "cpu": "arm64" }, "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw=="],
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.46.2", "", { "os": "android", "cpu": "arm64" }, "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.40.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w=="],
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.46.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.40.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ=="],
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.46.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.40.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ=="],
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.46.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.40.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q=="],
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.46.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.40.2", "", { "os": "linux", "cpu": "arm" }, "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q=="],
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.46.2", "", { "os": "linux", "cpu": "arm" }, "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.40.2", "", { "os": "linux", "cpu": "arm" }, "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg=="],
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.46.2", "", { "os": "linux", "cpu": "arm" }, "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.40.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg=="],
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.46.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.40.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg=="],
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.46.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg=="],
|
||||
|
||||
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.40.2", "", { "os": "linux", "cpu": "none" }, "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw=="],
|
||||
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.46.2", "", { "os": "linux", "cpu": "none" }, "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA=="],
|
||||
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.40.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q=="],
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.46.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.40.2", "", { "os": "linux", "cpu": "none" }, "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg=="],
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.46.2", "", { "os": "linux", "cpu": "none" }, "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.40.2", "", { "os": "linux", "cpu": "none" }, "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg=="],
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.46.2", "", { "os": "linux", "cpu": "none" }, "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.40.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ=="],
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.46.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.40.2", "", { "os": "linux", "cpu": "x64" }, "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng=="],
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.46.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.40.2", "", { "os": "linux", "cpu": "x64" }, "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA=="],
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.46.2", "", { "os": "linux", "cpu": "x64" }, "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.40.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg=="],
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.46.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.40.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA=="],
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.46.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.40.2", "", { "os": "win32", "cpu": "x64" }, "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA=="],
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.46.2", "", { "os": "win32", "cpu": "x64" }, "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg=="],
|
||||
|
||||
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.8", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.8" } }, "sha512-OWwBsbC9BFAJelmnNcrKuf+bka2ZxCE2A4Ft53Tkg4uoiE67r/PMEYwCsourC26E+kmxfwE0hVzMdxqeW+xu7Q=="],
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.13", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.5.1", "lightningcss": "1.30.1", "magic-string": "^0.30.18", "source-map-js": "^1.2.1", "tailwindcss": "4.1.13" } }, "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.8", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.8", "@tailwindcss/oxide-darwin-arm64": "4.1.8", "@tailwindcss/oxide-darwin-x64": "4.1.8", "@tailwindcss/oxide-freebsd-x64": "4.1.8", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.8", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.8", "@tailwindcss/oxide-linux-arm64-musl": "4.1.8", "@tailwindcss/oxide-linux-x64-gnu": "4.1.8", "@tailwindcss/oxide-linux-x64-musl": "4.1.8", "@tailwindcss/oxide-wasm32-wasi": "4.1.8", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.8", "@tailwindcss/oxide-win32-x64-msvc": "4.1.8" } }, "sha512-d7qvv9PsM5N3VNKhwVUhpK6r4h9wtLkJ6lz9ZY9aeZgrUWk1Z8VPyqyDT9MZlem7GTGseRQHkeB1j3tC7W1P+A=="],
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.13", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.13", "@tailwindcss/oxide-darwin-arm64": "4.1.13", "@tailwindcss/oxide-darwin-x64": "4.1.13", "@tailwindcss/oxide-freebsd-x64": "4.1.13", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13", "@tailwindcss/oxide-linux-arm64-musl": "4.1.13", "@tailwindcss/oxide-linux-x64-gnu": "4.1.13", "@tailwindcss/oxide-linux-x64-musl": "4.1.13", "@tailwindcss/oxide-wasm32-wasi": "4.1.13", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13", "@tailwindcss/oxide-win32-x64-msvc": "4.1.13" } }, "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA=="],
|
||||
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.8", "", { "os": "android", "cpu": "arm64" }, "sha512-Fbz7qni62uKYceWYvUjRqhGfZKwhZDQhlrJKGtnZfuNtHFqa8wmr+Wn74CTWERiW2hn3mN5gTpOoxWKk0jRxjg=="],
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.13", "", { "os": "android", "cpu": "arm64" }, "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RdRvedGsT0vwVVDztvyXhKpsU2ark/BjgG0huo4+2BluxdXo8NDgzl77qh0T1nUxmM11eXwR8jA39ibvSTbi7A=="],
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-t6PgxjEMLp5Ovf7uMb2OFmb3kqzVTPPakWpBIFzppk4JE4ix0yEtbtSjPbU8+PZETpaYMtXvss2Sdkx8Vs4XRw=="],
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw=="],
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-g8C8eGEyhHTqwPStSwZNSrOlyx0bhK/V/+zX0Y+n7DoRUzyS8eMbVshVOLJTDDC+Qn9IJnilYbIKzpB9n4aBsg=="],
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.13", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.8", "", { "os": "linux", "cpu": "arm" }, "sha512-Jmzr3FA4S2tHhaC6yCjac3rGf7hG9R6Gf2z9i9JFcuyy0u79HfQsh/thifbYTF2ic82KJovKKkIB6Z9TdNhCXQ=="],
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13", "", { "os": "linux", "cpu": "arm" }, "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-qq7jXtO1+UEtCmCeBBIRDrPFIVI4ilEQ97qgBGdwXAARrUqSn/L9fUrkb1XP/mvVtoVeR2bt/0L77xx53bPZ/Q=="],
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-O6b8QesPbJCRshsNApsOIpzKt3ztG35gfX9tEf4arD7mwNinsoCKxkj8TgEE0YRjmjtO3r9FlJnT/ENd9EVefQ=="],
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-32iEXX/pXwikshNOGnERAFwFSfiltmijMIAbUhnNyjFr3tmWmMJWQKU2vNcFX0DACSXJ3ZWcSkzNbaKTdngH6g=="],
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.13", "", { "os": "linux", "cpu": "x64" }, "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-s+VSSD+TfZeMEsCaFaHTaY5YNj3Dri8rST09gMvYQKwPphacRG7wbuQ5ZJMIJXN/puxPcg/nU+ucvWguPpvBDg=="],
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.13", "", { "os": "linux", "cpu": "x64" }, "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.8", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.10", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-CXBPVFkpDjM67sS1psWohZ6g/2/cd+cq56vPxK4JeawelxwK4YECgl9Y9TjkE2qfF+9/s1tHHJqrC4SS6cVvSg=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.13", "", { "dependencies": { "@emnapi/core": "^1.4.5", "@emnapi/runtime": "^1.4.5", "@emnapi/wasi-threads": "^1.0.4", "@napi-rs/wasm-runtime": "^0.2.12", "@tybys/wasm-util": "^0.10.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-7GmYk1n28teDHUjPlIx4Z6Z4hHEgvP5ZW2QS9ygnDAdI/myh3HTHjDqtSqgu1BpRoI4OiLx+fThAyA1JePoENA=="],
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.8", "", { "os": "win32", "cpu": "x64" }, "sha512-fou+U20j+Jl0EHwK92spoWISON2OBnCazIc038Xj2TdweYV33ZRkS9nwqiUi2d/Wba5xg5UoHfvynnb/UB49cQ=="],
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.13", "", { "os": "win32", "cpu": "x64" }, "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw=="],
|
||||
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.8", "", { "dependencies": { "@tailwindcss/node": "4.1.8", "@tailwindcss/oxide": "4.1.8", "tailwindcss": "4.1.8" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-CQ+I8yxNV5/6uGaJjiuymgw0kEQiNKRinYbZXPdx1fk5WgiyReG0VaUx/Xq6aVNSUNJFzxm6o8FNKS5aMaim5A=="],
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.13", "", { "dependencies": { "@tailwindcss/node": "4.1.13", "@tailwindcss/oxide": "4.1.13", "tailwindcss": "4.1.13" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ=="],
|
||||
|
||||
"@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.78.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.18.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-hYkhWr3UP0CkAsn/phBVR98UQawbw8CmTSgWtdgEBUjI60/GBaEIkpgi/Bp/2I8eIDK4+vdY7ac6jZx+GR+hEQ=="],
|
||||
"@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.89.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.37.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-vz8TEuw9GO0xXIdreMpcofvOY17T3cjgob9bSFln8yQsKsbsUvtpvV3F8pVC3tZEDq0IwO++3/e0/+7YKEarNA=="],
|
||||
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.79.0", "", {}, "sha512-s+epTqqLM0/TbJzMAK7OEhZIzh63P9sWz5HEFc5XHL4FvKQXQkcjI8F3nee+H/xVVn7mrP610nVXwOytTSYd0w=="],
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.89.0", "", {}, "sha512-joFV1MuPhSLsKfTzwjmPDrp8ENfZ9N23ymFu07nLfn3JCkSHy0CFgsyhHTJOmWaumC/WiNIKM0EJyduCF/Ih/Q=="],
|
||||
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.79.0", "", { "dependencies": { "@tanstack/query-core": "5.79.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-DjC4JIYZnYzxaTzbg3osOU63VNLP67dOrWet2cZvXgmgwAXNxfS52AMq86M5++ILuzW+BqTUEVMTjhrZ7/XBuA=="],
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.89.0", "", { "dependencies": { "@tanstack/query-core": "5.89.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-SXbtWSTSRXyBOe80mszPxpEbaN4XPRUp/i0EfQK1uyj3KCk/c8FuPJNIRwzOVe/OU3rzxrYtiNabsAmk1l714A=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
@@ -342,7 +343,7 @@
|
||||
|
||||
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
|
||||
|
||||
@@ -354,41 +355,39 @@
|
||||
|
||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||
|
||||
"@types/node": ["@types/node@22.15.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-5fF+eu5mwihV2BeVtX5vijhdaZOfkQTATrePEaXTcKqI16LhJ7gi2/Vhd9OZM0UojcdmiOCVg5rrax+i1MdoQQ=="],
|
||||
"@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.6", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q=="],
|
||||
"@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.1.5", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg=="],
|
||||
|
||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
"@types/react-dom": ["@types/react-dom@19.1.9", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ=="],
|
||||
|
||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.33.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.33.0", "@typescript-eslint/type-utils": "8.33.0", "@typescript-eslint/utils": "8.33.0", "@typescript-eslint/visitor-keys": "8.33.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.33.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ=="],
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.44.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/type-utils": "8.44.0", "@typescript-eslint/utils": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.44.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-EGDAOGX+uwwekcS0iyxVDmRV9HX6FLSM5kzrAToLTsr9OWCIKG/y3lQheCq18yZ5Xh78rRKJiEpP0ZaCs4ryOQ=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.33.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.33.0", "@typescript-eslint/types": "8.33.0", "@typescript-eslint/typescript-estree": "8.33.0", "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ=="],
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.44.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/types": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw=="],
|
||||
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.33.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.33.0", "@typescript-eslint/types": "^8.33.0", "debug": "^4.3.4" } }, "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A=="],
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.44.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.44.0", "@typescript-eslint/types": "^8.44.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ZeaGNraRsq10GuEohKTo4295Z/SuGcSq2LzfGlqiuEvfArzo/VRrT0ZaJsVPuKZ55lVbNk8U6FcL+ZMH8CoyVA=="],
|
||||
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.32.0", "", { "dependencies": { "@typescript-eslint/types": "8.32.0", "@typescript-eslint/visitor-keys": "8.32.0" } }, "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ=="],
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0" } }, "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg=="],
|
||||
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.33.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug=="],
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.44.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-x5Y0+AuEPqAInc6yd0n5DAcvtoQ/vyaGwuX5HE9n6qAefk1GaedqrLQF8kQGylLUb9pnZyLf+iEiL9fr8APDtQ=="],
|
||||
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.33.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.33.0", "@typescript-eslint/utils": "8.33.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ=="],
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.44.0", "", { "dependencies": { "@typescript-eslint/types": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0", "@typescript-eslint/utils": "8.44.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-9cwsoSxJ8Sak67Be/hD2RNt/fsqmWnNE1iHohG8lxqLSNY8xNfyY7wloo5zpW3Nu9hxVgURevqfcH6vvKCt6yg=="],
|
||||
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.32.0", "", {}, "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA=="],
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.32.0", "", { "dependencies": { "@typescript-eslint/types": "8.32.0", "@typescript-eslint/visitor-keys": "8.32.0", "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 <5.9.0" } }, "sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ=="],
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.44.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.44.0", "@typescript-eslint/tsconfig-utils": "8.44.0", "@typescript-eslint/types": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0", "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-lqNj6SgnGcQZwL4/SBJ3xdPEfcBuhCG8zdcwCPgYcmiPLgokiNDKlbPzCwEwu7m279J/lBYWtDYL+87OEfn8Jw=="],
|
||||
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.32.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.32.0", "@typescript-eslint/types": "8.32.0", "@typescript-eslint/typescript-estree": "8.32.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw=="],
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.43.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/types": "8.43.0", "@typescript-eslint/typescript-estree": "8.43.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.33.0", "", { "dependencies": { "@typescript-eslint/types": "8.33.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ=="],
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.44.0", "", { "dependencies": { "@typescript-eslint/types": "8.44.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-zaz9u8EJ4GBmnehlrpoKvj/E3dNbuQ7q0ucyZImm3cLqJ8INTc970B1qEqDX/Rzq65r3TvVTN7kHWPBoyW7DWw=="],
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.5.0", "", { "dependencies": { "@babel/core": "^7.26.10", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@rolldown/pluginutils": "1.0.0-beta.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "sha512-JuLWaEqypaJmOJPLWwO335Ig6jSgC1FTONCWAxnqcQthLTK/Yc9aH6hr9z/87xciejbQcnP3GnA1FWUSWeXaeg=="],
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.0.3", "", { "dependencies": { "@babel/core": "^7.28.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.35", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-PFVHhosKkofGH0Yzrw1BipSedTH68BFF8ZWy1kfUpCtJcouXXY0+racG8sExw7hw0HoX36813ga5o3LTWZ4FUg=="],
|
||||
|
||||
"acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
@@ -402,7 +401,7 @@
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"axios": ["axios@1.9.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg=="],
|
||||
"axios": ["axios@1.12.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw=="],
|
||||
|
||||
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
||||
|
||||
@@ -472,13 +471,11 @@
|
||||
|
||||
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||
|
||||
"dompurify": ["dompurify@3.2.6", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.151", "", {}, "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="],
|
||||
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
@@ -494,17 +491,17 @@
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint": ["eslint@9.27.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.27.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q=="],
|
||||
"eslint": ["eslint@9.35.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.35.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg=="],
|
||||
|
||||
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.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-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="],
|
||||
|
||||
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.20", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@8.3.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ=="],
|
||||
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
||||
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
||||
|
||||
"espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="],
|
||||
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
|
||||
|
||||
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
|
||||
|
||||
@@ -528,7 +525,7 @@
|
||||
|
||||
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
||||
|
||||
"fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="],
|
||||
"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=="],
|
||||
|
||||
@@ -542,7 +539,7 @@
|
||||
|
||||
"follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
|
||||
|
||||
"form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="],
|
||||
"form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
@@ -558,7 +555,7 @@
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"globals": ["globals@16.2.0", "", {}, "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg=="],
|
||||
"globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
@@ -582,9 +579,9 @@
|
||||
|
||||
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
|
||||
|
||||
"i18next": ["i18next@25.2.1", "", { "dependencies": { "@babel/runtime": "^7.27.1" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-+UoXK5wh+VlE1Zy5p6MjcvctHXAhRwQKCxiJD8noKZzIXmnAX8gdHX5fLPA3MEVxEN4vbZkQFy8N0LyD9tUqPw=="],
|
||||
"i18next": ["i18next@25.5.2", "", { "dependencies": { "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-lW8Zeh37i/o0zVr+NoCHfNnfvVw+M6FQbRp36ZZ/NyHDJ3NJVpp2HhAUyU9WafL5AssymNoOjMRB48mmx2P6Hw=="],
|
||||
|
||||
"i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.1.0", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-mHZxNx1Lq09xt5kCauZ/4bsXOEA2pfpwSoU11/QTJB+pD94iONFwp+ohqi///PwiFvjFOxe1akYCdHyFo1ng5Q=="],
|
||||
"i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.0", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g=="],
|
||||
|
||||
"i18next-resources-to-backend": ["i18next-resources-to-backend@1.2.1", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw=="],
|
||||
|
||||
@@ -636,27 +633,27 @@
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.29.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.29.2", "lightningcss-darwin-x64": "1.29.2", "lightningcss-freebsd-x64": "1.29.2", "lightningcss-linux-arm-gnueabihf": "1.29.2", "lightningcss-linux-arm64-gnu": "1.29.2", "lightningcss-linux-arm64-musl": "1.29.2", "lightningcss-linux-x64-gnu": "1.29.2", "lightningcss-linux-x64-musl": "1.29.2", "lightningcss-win32-arm64-msvc": "1.29.2", "lightningcss-win32-x64-msvc": "1.29.2" } }, "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA=="],
|
||||
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.29.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA=="],
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
|
||||
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.29.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w=="],
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="],
|
||||
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.29.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg=="],
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="],
|
||||
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.29.2", "", { "os": "linux", "cpu": "arm" }, "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg=="],
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="],
|
||||
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.29.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ=="],
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="],
|
||||
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.29.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ=="],
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="],
|
||||
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.29.2", "", { "os": "linux", "cpu": "x64" }, "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg=="],
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="],
|
||||
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.29.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w=="],
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="],
|
||||
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.29.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw=="],
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="],
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.29.2", "", { "os": "win32", "cpu": "x64" }, "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA=="],
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
|
||||
@@ -666,9 +663,9 @@
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"lucide-react": ["lucide-react@0.511.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w=="],
|
||||
"lucide-react": ["lucide-react@0.544.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||
"magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
@@ -772,13 +769,13 @@
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
|
||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
"prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
|
||||
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
||||
|
||||
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
||||
|
||||
@@ -788,13 +785,13 @@
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
|
||||
"react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="],
|
||||
|
||||
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
|
||||
"react-dom": ["react-dom@19.1.1", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.1" } }, "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw=="],
|
||||
|
||||
"react-hook-form": ["react-hook-form@7.56.4", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-Rob7Ftz2vyZ/ZGsQZPaRdIefkgOSrQSPXfqBdvOPwJfoGnjwRJUs7EM7Kc1mcoDv3NOtqBzPGbcMB8CGn9CKgw=="],
|
||||
"react-hook-form": ["react-hook-form@7.62.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA=="],
|
||||
|
||||
"react-i18next": ["react-i18next@15.5.2", "", { "dependencies": { "@babel/runtime": "^7.25.0", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-ePODyXgmZQAOYTbZXQn5rRsSBu3Gszo69jxW6aKmlSgxKAI1fOhDwSu6bT4EKHciWPKQ7v7lPrjeiadR6Gi+1A=="],
|
||||
"react-i18next": ["react-i18next@15.7.3", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 25.4.1", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-AANws4tOE+QSq/IeMF/ncoHlMNZaVLxpa5uUGW1wjike68elVYr0018L9xYoqBr1OFO7G7boDPrbn0HpMCJxTw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -804,7 +801,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-router": ["react-router@7.6.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-hPJXXxHJZEsPFNVbtATH7+MMX43UDeOauz+EAU4cgqTn7ojdI9qQORqS8Z0qmDlL1TclO/6jLRYUEtbWidtdHQ=="],
|
||||
"react-router": ["react-router@7.9.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-pfAByjcTpX55mqSDGwGnY9vDCpxqBLASg0BMNAuMmpSGESo/TaOUG6BllhAtAkCGx8Rnohik/XtaqiYUJtgW2g=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -816,7 +813,7 @@
|
||||
|
||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"rollup": ["rollup@4.40.2", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.40.2", "@rollup/rollup-android-arm64": "4.40.2", "@rollup/rollup-darwin-arm64": "4.40.2", "@rollup/rollup-darwin-x64": "4.40.2", "@rollup/rollup-freebsd-arm64": "4.40.2", "@rollup/rollup-freebsd-x64": "4.40.2", "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", "@rollup/rollup-linux-arm-musleabihf": "4.40.2", "@rollup/rollup-linux-arm64-gnu": "4.40.2", "@rollup/rollup-linux-arm64-musl": "4.40.2", "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", "@rollup/rollup-linux-riscv64-gnu": "4.40.2", "@rollup/rollup-linux-riscv64-musl": "4.40.2", "@rollup/rollup-linux-s390x-gnu": "4.40.2", "@rollup/rollup-linux-x64-gnu": "4.40.2", "@rollup/rollup-linux-x64-musl": "4.40.2", "@rollup/rollup-win32-arm64-msvc": "4.40.2", "@rollup/rollup-win32-ia32-msvc": "4.40.2", "@rollup/rollup-win32-x64-msvc": "4.40.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg=="],
|
||||
"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=="],
|
||||
|
||||
@@ -830,7 +827,7 @@
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"sonner": ["sonner@2.0.3", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA=="],
|
||||
"sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
@@ -846,15 +843,15 @@
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"tailwind-merge": ["tailwind-merge@3.3.0", "", {}, "sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ=="],
|
||||
"tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.1.8", "", {}, "sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og=="],
|
||||
"tailwindcss": ["tailwindcss@4.1.13", "", {}, "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w=="],
|
||||
|
||||
"tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="],
|
||||
|
||||
"tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="],
|
||||
"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=="],
|
||||
|
||||
@@ -866,15 +863,15 @@
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"tw-animate-css": ["tw-animate-css@1.3.2", "", {}, "sha512-khGYcg4sHWFWcjpiWvy0KN0Bd6yVy6Ecc4r9ZP2u7FV+n4/Fp8MQscCWJkM0KMIRvrpGyKpIQnIbEd1hrewdeg=="],
|
||||
"tw-animate-css": ["tw-animate-css@1.3.8", "", {}, "sha512-Qrk3PZ7l7wUcGYhwZloqfkWCmaXZAoqjkdbIDvzfGshwGtexa/DAs9koXxIkrpEasyevandomzCBAV1Yyop5rw=="],
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.33.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.33.0", "@typescript-eslint/parser": "8.33.0", "@typescript-eslint/utils": "8.33.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-5YmNhF24ylCsvdNW2oJwMzTbaeO4bg90KeGtMjUw0AGtHksgEPLRTUil+coHwCfiu4QjVJFnjp94DmU6zV7DhQ=="],
|
||||
"typescript-eslint": ["typescript-eslint@8.44.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.44.0", "@typescript-eslint/parser": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0", "@typescript-eslint/utils": "8.44.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ib7mCkYuIzYonCq9XWF5XNw+fkj2zg629PSa9KNIQ47RXFF763S5BIX4wqz1+FLPogTZoiw8KmCiRPRa8bL3qw=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
"undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="],
|
||||
|
||||
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
|
||||
|
||||
@@ -900,7 +897,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=="],
|
||||
|
||||
"vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "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-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="],
|
||||
"vite": ["vite@7.1.6", "", { "dependencies": { "esbuild": "^0.25.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-SRYIB8t/isTwNn8vMB3MR6E+EQZM/WG1aKmmIUCfDXfVvKfc20ZpamngWHKzAmmu9ppsgxsg4b2I7c90JZudIQ=="],
|
||||
|
||||
"void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="],
|
||||
|
||||
@@ -912,144 +909,152 @@
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"zod": ["zod@3.25.42", "", {}, "sha512-PcALTLskaucbeHc41tU/xfjfhcz8z0GdhhDcSgrCTmSazUuqnYqiXO63M0QUBVwpBlsLsNVn5qHSC5Dw3KZvaQ=="],
|
||||
"zod": ["zod@4.1.9", "", {}, "sha512-HI32jTq0AUAC125z30E8bQNz0RQ+9Uc+4J7V97gLYjZVKRjeydPgGt6dvQzFrav7MYOUGFqqOGiHpA/fdbd0cQ=="],
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
"@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
||||
"@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=="],
|
||||
|
||||
"@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/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/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/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/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-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@eslint/eslintrc/espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="],
|
||||
|
||||
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||
|
||||
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
|
||||
"@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
|
||||
"@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
|
||||
"@tailwindcss/node/jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" }, "bundled": true }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.10", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" }, "bundled": true }, "sha512-bCsCyeZEwVErsGmyPNSzwfwFn4OdxBj0mmv6hOFucB/k81Ojdu68RbZdxYsRQUPc9l6SU5F/cG+bXgWs3oUgsQ=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.4", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" }, "bundled": true }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.33.0", "", { "dependencies": { "@typescript-eslint/types": "8.33.0", "@typescript-eslint/visitor-keys": "8.33.0" } }, "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw=="],
|
||||
"@types/babel__core/@babel/parser": ["@babel/parser@7.27.2", "", { "dependencies": { "@babel/types": "^7.27.1" }, "bin": "./bin/babel-parser.js" }, "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.33.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.33.0", "@typescript-eslint/types": "8.33.0", "@typescript-eslint/typescript-estree": "8.33.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw=="],
|
||||
"@types/babel__core/@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=="],
|
||||
|
||||
"@types/babel__generator/@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=="],
|
||||
|
||||
"@types/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=="],
|
||||
|
||||
"@types/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=="],
|
||||
|
||||
"@types/babel__traverse/@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=="],
|
||||
|
||||
"@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.44.0", "", { "dependencies": { "@typescript-eslint/types": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0" } }, "sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.44.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/types": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.4", "", {}, "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="],
|
||||
|
||||
"@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.33.0", "", { "dependencies": { "@typescript-eslint/types": "8.33.0", "@typescript-eslint/visitor-keys": "8.33.0" } }, "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw=="],
|
||||
"@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.44.0", "", { "dependencies": { "@typescript-eslint/types": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0" } }, "sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA=="],
|
||||
|
||||
"@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.33.0", "", {}, "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg=="],
|
||||
"@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="],
|
||||
|
||||
"@typescript-eslint/parser/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.33.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.33.0", "@typescript-eslint/tsconfig-utils": "8.33.0", "@typescript-eslint/types": "8.33.0", "@typescript-eslint/visitor-keys": "8.33.0", "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 <5.9.0" } }, "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ=="],
|
||||
"@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="],
|
||||
|
||||
"@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.33.0", "", {}, "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg=="],
|
||||
"@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw=="],
|
||||
|
||||
"@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.32.0", "", { "dependencies": { "@typescript-eslint/types": "8.32.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w=="],
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="],
|
||||
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.33.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.33.0", "@typescript-eslint/tsconfig-utils": "8.33.0", "@typescript-eslint/types": "8.33.0", "@typescript-eslint/visitor-keys": "8.33.0", "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 <5.9.0" } }, "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ=="],
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.44.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/types": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg=="],
|
||||
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.33.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.33.0", "@typescript-eslint/types": "8.33.0", "@typescript-eslint/typescript-estree": "8.33.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.32.0", "", { "dependencies": { "@typescript-eslint/types": "8.32.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w=="],
|
||||
"@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="],
|
||||
|
||||
"@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/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.33.0", "", {}, "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg=="],
|
||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.43.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.43.0", "@typescript-eslint/tsconfig-utils": "8.43.0", "@typescript-eslint/types": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0", "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-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.33.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.33.0", "@typescript-eslint/types": "8.33.0", "@typescript-eslint/typescript-estree": "8.33.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw=="],
|
||||
"typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.44.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/types": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
|
||||
"@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=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="],
|
||||
"@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=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="],
|
||||
"@babel/helper-module-imports/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="],
|
||||
"@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=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="],
|
||||
"@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=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="],
|
||||
"@eslint/eslintrc/espree/acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="],
|
||||
"@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="],
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="],
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.33.0", "", {}, "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.33.0", "", {}, "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.33.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.33.0", "@typescript-eslint/tsconfig-utils": "8.33.0", "@typescript-eslint/types": "8.33.0", "@typescript-eslint/visitor-keys": "8.33.0", "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 <5.9.0" } }, "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ=="],
|
||||
|
||||
"@typescript-eslint/parser/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"@typescript-eslint/parser/@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
||||
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.33.0", "", {}, "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg=="],
|
||||
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
||||
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.33.0", "", { "dependencies": { "@typescript-eslint/types": "8.33.0", "@typescript-eslint/visitor-keys": "8.33.0" } }, "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw=="],
|
||||
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.33.0", "", {}, "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg=="],
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.44.0", "", { "dependencies": { "@typescript-eslint/types": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0" } }, "sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA=="],
|
||||
|
||||
"@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-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.33.0", "", { "dependencies": { "@typescript-eslint/types": "8.33.0", "@typescript-eslint/visitor-keys": "8.33.0" } }, "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw=="],
|
||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.43.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.43.0", "@typescript-eslint/types": "^8.43.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw=="],
|
||||
|
||||
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.33.0", "", {}, "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg=="],
|
||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.43.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA=="],
|
||||
|
||||
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.33.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.33.0", "@typescript-eslint/tsconfig-utils": "8.33.0", "@typescript-eslint/types": "8.33.0", "@typescript-eslint/visitor-keys": "8.33.0", "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 <5.9.0" } }, "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ=="],
|
||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@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=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
||||
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.44.0", "", { "dependencies": { "@typescript-eslint/types": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0" } }, "sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA=="],
|
||||
|
||||
"@typescript-eslint/parser/@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-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="],
|
||||
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||
"@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=="],
|
||||
|
||||
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
"@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/@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
||||
"@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=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@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=="],
|
||||
"@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"typescript-eslint/@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=="],
|
||||
"@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,49 +10,48 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@tailwindcss/vite": "^4.1.8",
|
||||
"@tanstack/react-query": "^5.79.0",
|
||||
"axios": "^1.9.0",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@tanstack/react-query": "^5.89.0",
|
||||
"axios": "^1.12.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dompurify": "^3.2.6",
|
||||
"i18next": "^25.2.1",
|
||||
"i18next-browser-languagedetector": "^8.0.5",
|
||||
"i18next": "^25.5.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.511.0",
|
||||
"lucide-react": "^0.544.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.56.4",
|
||||
"react-i18next": "^15.5.2",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-i18next": "^15.7.3",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router": "^7.6.1",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"zod": "^3.25.42"
|
||||
"react-router": "^7.9.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"zod": "^4.1.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.27.0",
|
||||
"@tanstack/eslint-plugin-query": "^5.78.0",
|
||||
"@types/node": "^22.15.27",
|
||||
"@types/react": "^19.1.6",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@vitejs/plugin-react": "^4.5.0",
|
||||
"eslint": "^9.27.0",
|
||||
"@eslint/js": "^9.35.0",
|
||||
"@tanstack/eslint-plugin-query": "^5.89.0",
|
||||
"@types/node": "^24.5.2",
|
||||
"@types/react": "^19.1.13",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@vitejs/plugin-react": "^5.0.3",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.2.0",
|
||||
"prettier": "3.5.3",
|
||||
"tw-animate-css": "^1.3.2",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.33.0",
|
||||
"vite": "^6.3.1"
|
||||
"globals": "^16.4.0",
|
||||
"prettier": "3.6.2",
|
||||
"tw-animate-css": "^1.3.8",
|
||||
"typescript": "~5.9.2",
|
||||
"typescript-eslint": "^8.44.0",
|
||||
"vite": "^7.1.6"
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,8 @@ export const App = () => {
|
||||
const { isLoggedIn } = useUserContext();
|
||||
|
||||
if (isLoggedIn) {
|
||||
return <Navigate to="/logout" />;
|
||||
return <Navigate to="/logout" replace />;
|
||||
}
|
||||
|
||||
return <Navigate to="/login" />;
|
||||
return <Navigate to="/login" replace />;
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "../ui/form";
|
||||
import { Button } from "../ui/button";
|
||||
import { loginSchema, LoginSchema } from "@/schemas/login-schema";
|
||||
import z from "zod";
|
||||
|
||||
interface Props {
|
||||
onSubmit: (data: LoginSchema) => void;
|
||||
@@ -22,6 +23,11 @@ export const LoginForm = (props: Props) => {
|
||||
const { onSubmit, loading } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
z.config({
|
||||
customError: (iss) =>
|
||||
iss.input === undefined ? t("fieldRequired") : t("invalidInput"),
|
||||
});
|
||||
|
||||
const form = useForm<LoginSchema>({
|
||||
resolver: zodResolver(loginSchema),
|
||||
});
|
||||
@@ -33,12 +39,13 @@ export const LoginForm = (props: Props) => {
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-4">
|
||||
<FormLabel>{t("loginUsername")}</FormLabel>
|
||||
<FormControl>
|
||||
<FormItem className="mb-4 gap-0">
|
||||
<FormLabel className="mb-2">{t("loginUsername")}</FormLabel>
|
||||
<FormControl className="mb-1">
|
||||
<Input
|
||||
placeholder={t("loginUsername")}
|
||||
disabled={loading}
|
||||
autoComplete="username"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -50,25 +57,26 @@ export const LoginForm = (props: Props) => {
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-4">
|
||||
<div className="relative">
|
||||
<FormItem className="mb-4 gap-0">
|
||||
<div className="relative mb-1">
|
||||
<FormLabel className="mb-2">{t("loginPassword")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t("loginPassword")}
|
||||
type="password"
|
||||
disabled={loading}
|
||||
autoComplete="current-password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<a
|
||||
href="/forgot-password"
|
||||
className="text-muted-foreground text-sm absolute right-0 bottom-10"
|
||||
className="text-muted-foreground text-sm absolute right-0 bottom-[2.565rem]" // 2.565 is *just* perfect
|
||||
>
|
||||
{t("forgotPasswordTitle")}
|
||||
</a>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { totpSchema, TotpSchema } from "@/schemas/totp-schema";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import z from "zod";
|
||||
|
||||
interface Props {
|
||||
formId: string;
|
||||
@@ -17,6 +19,12 @@ interface Props {
|
||||
|
||||
export const TotpForm = (props: Props) => {
|
||||
const { formId, onSubmit, loading } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
z.config({
|
||||
customError: (iss) =>
|
||||
iss.input === undefined ? t("fieldRequired") : t("invalidInput"),
|
||||
});
|
||||
|
||||
const form = useForm<TotpSchema>({
|
||||
resolver: zodResolver(totpSchema),
|
||||
@@ -31,7 +39,12 @@ export const TotpForm = (props: Props) => {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<InputOTP maxLength={6} disabled={loading} {...field}>
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
disabled={loading}
|
||||
{...field}
|
||||
autoComplete="one-time-code"
|
||||
>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
|
||||
56
frontend/src/components/domain-warning/domain-warning.tsx
Normal file
56
frontend/src/components/domain-warning/domain-warning.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "../ui/card";
|
||||
import { Button } from "../ui/button";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useLocation } from "react-router";
|
||||
|
||||
interface Props {
|
||||
onClick: () => void;
|
||||
appUrl: string;
|
||||
currentUrl: string;
|
||||
}
|
||||
|
||||
export const DomainWarning = (props: Props) => {
|
||||
const { onClick, appUrl, currentUrl } = props;
|
||||
const { t } = useTranslation();
|
||||
const { search } = useLocation();
|
||||
|
||||
const searchParams = new URLSearchParams(search);
|
||||
const redirectUri = searchParams.get("redirect_uri");
|
||||
|
||||
return (
|
||||
<Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-3xl">{t("domainWarningTitle")}</CardTitle>
|
||||
<CardDescription>
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey="domainWarningSubtitle"
|
||||
values={{ appUrl, currentUrl }}
|
||||
components={{ code: <code /> }}
|
||||
/>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex flex-col items-stretch gap-2">
|
||||
<Button onClick={onClick} variant="warning">
|
||||
{t("ignoreTitle")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
window.location.assign(
|
||||
`${appUrl}/login?redirect_uri=${encodeURIComponent(redirectUri || "")}`,
|
||||
)
|
||||
}
|
||||
variant="outline"
|
||||
>
|
||||
{t("goToCorrectDomainTitle")}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
18
frontend/src/components/icons/microsoft.tsx
Normal file
18
frontend/src/components/icons/microsoft.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function MicrosoftIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="2em"
|
||||
height="2em"
|
||||
viewBox="0 0 256 256"
|
||||
{...props}
|
||||
>
|
||||
<path fill="#f1511b" d="M121.666 121.666H0V0h121.666z"></path>
|
||||
<path fill="#80cc28" d="M256 121.666H134.335V0H256z"></path>
|
||||
<path fill="#00adef" d="M121.663 256.002H0V134.336h121.663z"></path>
|
||||
<path fill="#fbbc09" d="M256 256.002H134.335V134.336H256z"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function GenericIcon(props: SVGProps<SVGSVGElement>) {
|
||||
export function OAuthIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
20
frontend/src/components/icons/pocket-id.tsx
Normal file
20
frontend/src/components/icons/pocket-id.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function PocketIDIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlSpace="preserve"
|
||||
width={512}
|
||||
height={512}
|
||||
viewBox="0 0 512 512"
|
||||
{...props}
|
||||
>
|
||||
<circle cx="256" cy="256" r="256" />
|
||||
<path
|
||||
d="M268.6 102.4c64.4 0 116.8 52.4 116.8 116.7 0 25.3-8 49.4-23 69.6-14.8 19.9-35 34.3-58.4 41.7l-6.5 2-15.5-76.2 4.3-2c14-6.7 23-21.1 23-36.6 0-22.4-18.2-40.6-40.6-40.6S228 195.2 228 217.6c0 15.5 9 29.8 23 36.6l4.2 2-25 153.4h-69.5V102.4z"
|
||||
className="fill-white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
26
frontend/src/components/icons/tailscale.tsx
Normal file
26
frontend/src/components/icons/tailscale.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function TailscaleIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlSpace="preserve"
|
||||
width={512}
|
||||
height={512}
|
||||
viewBox="0 0 512 512"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
className="opacity-80"
|
||||
fill="currentColor"
|
||||
d="M65.6 318.1c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9S1.8 219 1.8 254.2s28.6 63.9 63.8 63.9m191.6 0c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9m0 193.9c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9m189.2-193.9c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9"
|
||||
/>
|
||||
|
||||
<path
|
||||
d="M65.6 127.7c35.3 0 63.9-28.6 63.9-63.9S100.9 0 65.6 0 1.8 28.6 1.8 63.9s28.6 63.8 63.8 63.8m0 384.3c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.8 28.7-63.8 63.9S30.4 512 65.6 512m191.6-384.3c35.3 0 63.9-28.6 63.9-63.9S292.5 0 257.2 0s-63.9 28.6-63.9 63.9 28.6 63.8 63.9 63.8m189.2 0c35.3 0 63.9-28.6 63.9-63.9S481.6 0 446.4 0c-35.3 0-63.9 28.6-63.9 63.9s28.6 63.8 63.9 63.8m0 384.3c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9"
|
||||
className="opacity-20"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useAppContext } from "@/context/app-context";
|
||||
import { LanguageSelector } from "../language/language";
|
||||
import { Outlet } from "react-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import { DomainWarning } from "../domain-warning/domain-warning";
|
||||
|
||||
export const Layout = () => {
|
||||
const BaseLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
const { backgroundImage } = useAppContext();
|
||||
|
||||
return (
|
||||
@@ -15,7 +17,38 @@ export const Layout = () => {
|
||||
}}
|
||||
>
|
||||
<LanguageSelector />
|
||||
<Outlet />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Layout = () => {
|
||||
const { appUrl } = useAppContext();
|
||||
const [ignoreDomainWarning, setIgnoreDomainWarning] = useState(() => {
|
||||
return window.sessionStorage.getItem("ignoreDomainWarning") === "true";
|
||||
});
|
||||
const currentUrl = window.location.origin;
|
||||
|
||||
const handleIgnore = useCallback(() => {
|
||||
window.sessionStorage.setItem("ignoreDomainWarning", "true");
|
||||
setIgnoreDomainWarning(true);
|
||||
}, [setIgnoreDomainWarning]);
|
||||
|
||||
if (!ignoreDomainWarning && appUrl !== currentUrl) {
|
||||
return (
|
||||
<BaseLayout>
|
||||
<DomainWarning
|
||||
appUrl={appUrl}
|
||||
currentUrl={currentUrl}
|
||||
onClick={() => handleIgnore()}
|
||||
/>
|
||||
</BaseLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseLayout>
|
||||
<Outlet />
|
||||
</BaseLayout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ const buttonVariants = cva(
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
warning:
|
||||
"bg-amber-500 text-white shadow-xs hover:bg-amber-400 focus-visible:ring-amber-200/20 dark:focus-visible:ring-amber-400/40 dark:bg-amber-600",
|
||||
"bg-amber-500 text-white shadow-xs hover:bg-amber-400 focus-visible:ring-amber-200/20 dark:focus-visible:ring-amber-400/40",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
|
||||
@@ -15,7 +15,7 @@ export const AppContextProvider = ({
|
||||
}) => {
|
||||
const { isFetching, data, error } = useSuspenseQuery({
|
||||
queryKey: ["app"],
|
||||
queryFn: () => axios.get("/api/app").then((res) => res.data),
|
||||
queryFn: () => axios.get("/api/context/app").then((res) => res.data),
|
||||
});
|
||||
|
||||
if (error && !isFetching) {
|
||||
|
||||
@@ -15,7 +15,7 @@ export const UserContextProvider = ({
|
||||
}) => {
|
||||
const { isFetching, data, error } = useSuspenseQuery({
|
||||
queryKey: ["user"],
|
||||
queryFn: () => axios.get("/api/user").then((res) => res.data),
|
||||
queryFn: () => axios.get("/api/context/user").then((res) => res.data),
|
||||
});
|
||||
|
||||
if (error && !isFetching) {
|
||||
|
||||
@@ -136,7 +136,7 @@ h4 {
|
||||
}
|
||||
|
||||
p {
|
||||
@apply leading-6 [&:not(:first-child)]:mt-6;
|
||||
@apply leading-6;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
@@ -156,7 +156,7 @@ ul {
|
||||
}
|
||||
|
||||
code {
|
||||
@apply relative rounded bg-muted px-[0.2rem] py-[0.1rem] font-mono text-sm font-semibold;
|
||||
@apply relative rounded bg-muted px-[0.2rem] py-[0.1rem] font-mono text-sm font-semibold break-all;
|
||||
}
|
||||
|
||||
.lead {
|
||||
|
||||
@@ -27,8 +27,8 @@ export const languages = {
|
||||
"tr-TR": "Türkçe",
|
||||
"uk-UA": "Українська",
|
||||
"vi-VN": "Tiếng Việt",
|
||||
"zh-CN": "中文",
|
||||
"zh-TW": "中文",
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "繁體中文(台灣)",
|
||||
};
|
||||
|
||||
export type SupportedLanguage = keyof typeof languages;
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Try again",
|
||||
"untrustedRedirectTitle": "Untrusted redirect",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
@@ -49,5 +50,8 @@
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"loginTitle": "مرحبا بعودتك، قم بتسجيل الدخول باستخدام",
|
||||
"loginTitleSimple": "Welcome back, please login",
|
||||
"loginDivider": "Or",
|
||||
"loginTitle": "مرحبا بعودتك، ادخل باستخدام",
|
||||
"loginTitleSimple": "مرحبا بعودتك، سجل دخولك",
|
||||
"loginDivider": "أو",
|
||||
"loginUsername": "اسم المستخدم",
|
||||
"loginPassword": "كلمة المرور",
|
||||
"loginSubmit": "تسجيل الدخول",
|
||||
@@ -10,8 +10,8 @@
|
||||
"loginFailRateLimit": "You failed to login too many times. Please try again later",
|
||||
"loginSuccessTitle": "تم تسجيل الدخول",
|
||||
"loginSuccessSubtitle": "مرحبا بعودتك!",
|
||||
"loginOauthFailTitle": "An error occurred",
|
||||
"loginOauthFailSubtitle": "فشل في الحصول على رابط OAuth",
|
||||
"loginOauthFailTitle": "حدث خطأ",
|
||||
"loginOauthFailSubtitle": "أخفق الحصول على رابط OAuth",
|
||||
"loginOauthSuccessTitle": "إعادة توجيه",
|
||||
"loginOauthSuccessSubtitle": "إعادة توجيه إلى مزود OAuth الخاص بك",
|
||||
"continueRedirectingTitle": "إعادة توجيه...",
|
||||
@@ -19,7 +19,7 @@
|
||||
"continueInvalidRedirectTitle": "إعادة توجيه غير صالحة",
|
||||
"continueInvalidRedirectSubtitle": "رابط إعادة التوجيه غير صالح",
|
||||
"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>، هل أنت متأكد أنك تريد المتابعة؟",
|
||||
"continueTitle": "متابعة",
|
||||
"continueSubtitle": "انقر الزر للمتابعة إلى التطبيق الخاص بك.",
|
||||
"logoutFailTitle": "فشل تسجيل الخروج",
|
||||
@@ -32,7 +32,7 @@
|
||||
"notFoundTitle": "الصفحة غير موجودة",
|
||||
"notFoundSubtitle": "الصفحة التي تبحث عنها غير موجودة.",
|
||||
"notFoundButton": "انتقل إلى الرئيسية",
|
||||
"totpFailTitle": "فشل في التحقق من الرمز",
|
||||
"totpFailTitle": "أخفق التحقق من الرمز",
|
||||
"totpFailSubtitle": "الرجاء التحقق من الرمز الخاص بك وحاول مرة أخرى",
|
||||
"totpSuccessTitle": "تم التحقق",
|
||||
"totpSuccessSubtitle": "إعادة توجيه إلى تطبيقك",
|
||||
@@ -42,12 +42,16 @@
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "حاول مجددا",
|
||||
"untrustedRedirectTitle": "Untrusted redirect",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
"untrustedRedirectTitle": "إعادة توجيه غير موثوقة",
|
||||
"untrustedRedirectSubtitle": "أنت تحاول إعادة التوجيه إلى نطاق لا يتطابق مع النطاق المكون الخاص بك (<code>{{domain}}</code>). هل أنت متأكد من أنك تريد المتابعة؟",
|
||||
"cancelTitle": "إلغاء",
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"forgotPasswordTitle": "نسيت كلمة المرور؟",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"errorTitle": "حدث خطأ",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Try again",
|
||||
"untrustedRedirectTitle": "Untrusted redirect",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
@@ -49,5 +50,8 @@
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Try again",
|
||||
"untrustedRedirectTitle": "Untrusted redirect",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
@@ -49,5 +50,8 @@
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
"unauthorizedResourceSubtitle": "Brugeren med brugernavnet <code>{{username}}</code> har ikke tilladelse til at tilgå ressourcen <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "Brugeren med brugernavnet <code>{{username}}</code> har ikke tilladelse til at logge ind.",
|
||||
"unauthorizedGroupsSubtitle": "Brugeren med brugernavnet <code>{{username}}</code> er ikke i de grupper, som ressourcen <code>{{resource}}</code> kræver.",
|
||||
"unauthorizedIpSubtitle": "Din IP adresse <code>{{ip}}</code> er ikke autoriseret til at tilgå ressourcen <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Prøv igen",
|
||||
"untrustedRedirectTitle": "Usikker omdirigering",
|
||||
"untrustedRedirectSubtitle": "Du forsøger at omdirigere til et domæne, der ikke matcher dit konfigurerede domæne (<code>{{domain}}</code>). Er du sikker på, at du vil fortsætte?",
|
||||
@@ -49,5 +50,8 @@
|
||||
"forgotPasswordTitle": "Glemt din adgangskode?",
|
||||
"failedToFetchProvidersTitle": "Kunne ikke indlæse godkendelsesudbydere. Tjek venligst din konfiguration.",
|
||||
"errorTitle": "Der opstod en fejl",
|
||||
"errorSubtitle": "Der opstod en fejl under forsøget på at udføre denne handling. Tjek venligst konsollen for mere information."
|
||||
"errorSubtitle": "Der opstod en fejl under forsøget på at udføre denne handling. Tjek venligst konsollen for mere information.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"loginTitle": "Willkommen zurück, logge dich ein mit",
|
||||
"loginTitleSimple": "Welcome back, please login",
|
||||
"loginDivider": "Or",
|
||||
"loginTitleSimple": "Willkommen zurück, bitte anmelden",
|
||||
"loginDivider": "Oder",
|
||||
"loginUsername": "Benutzername",
|
||||
"loginPassword": "Passwort",
|
||||
"loginSubmit": "Anmelden",
|
||||
"loginFailTitle": "Login fehlgeschlagen",
|
||||
"loginFailSubtitle": "Bitte überprüfe deinen Benutzernamen und Passwort",
|
||||
"loginFailRateLimit": "You failed to login too many times. Please try again later",
|
||||
"loginFailRateLimit": "Zu viele fehlgeschlagene Loginversuche. Versuche es später erneut",
|
||||
"loginSuccessTitle": "Angemeldet",
|
||||
"loginSuccessSubtitle": "Willkommen zurück!",
|
||||
"loginOauthFailTitle": "An error occurred",
|
||||
"loginOauthFailTitle": "Ein Fehler ist aufgetreten",
|
||||
"loginOauthFailSubtitle": "Fehler beim Abrufen der OAuth-URL",
|
||||
"loginOauthSuccessTitle": "Leite weiter",
|
||||
"loginOauthSuccessSubtitle": "Weiterleitung zu Ihrem OAuth-Provider",
|
||||
@@ -19,7 +19,7 @@
|
||||
"continueInvalidRedirectTitle": "Ungültige Weiterleitung",
|
||||
"continueInvalidRedirectSubtitle": "Die Weiterleitungs-URL ist ungültig",
|
||||
"continueInsecureRedirectTitle": "Unsichere Weiterleitung",
|
||||
"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": "Sie versuchen von <code>https</code> auf <code>http</code> weiterzuleiten, was unsicher ist. Sind Sie sicher, dass Sie fortfahren möchten?",
|
||||
"continueTitle": "Weiter",
|
||||
"continueSubtitle": "Klicken Sie auf den Button, um zur App zu gelangen.",
|
||||
"logoutFailTitle": "Abmelden fehlgeschlagen",
|
||||
@@ -27,8 +27,8 @@
|
||||
"logoutSuccessTitle": "Abgemeldet",
|
||||
"logoutSuccessSubtitle": "Sie wurden abgemeldet",
|
||||
"logoutTitle": "Abmelden",
|
||||
"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.",
|
||||
"logoutUsernameSubtitle": "Sie sind derzeit als <code>{{username}}</code> angemeldet. Klicken Sie auf den Button unten, um sich abzumelden.",
|
||||
"logoutOauthSubtitle": "Sie sind derzeit als <code>{{username}}</code> über den OAuth-Anbieter {{provider}} angemeldet. Klicken Sie auf den Button unten, um sich abzumelden.",
|
||||
"notFoundTitle": "Seite nicht gefunden",
|
||||
"notFoundSubtitle": "Die gesuchte Seite existiert nicht.",
|
||||
"notFoundButton": "Nach Hause",
|
||||
@@ -37,17 +37,21 @@
|
||||
"totpSuccessTitle": "Verifiziert",
|
||||
"totpSuccessSubtitle": "Leite zur App weiter",
|
||||
"totpTitle": "Geben Sie Ihren TOTP Code ein",
|
||||
"totpSubtitle": "Please enter the code from your authenticator app.",
|
||||
"totpSubtitle": "Bitte geben Sie den Code aus Ihrer Authenticator-App ein.",
|
||||
"unauthorizedTitle": "Unautorisiert",
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedResourceSubtitle": "Der Benutzer mit Benutzername <code>{{username}}</code> ist nicht berechtigt, auf die Ressource <code>{{resource}}</code> zuzugreifen.",
|
||||
"unauthorizedLoginSubtitle": "Der Benutzer mit Benutzername <code>{{username}}</code> ist nicht berechtigt, sich anzumelden.",
|
||||
"unauthorizedGroupsSubtitle": "Der Benutzer mit Benutzername <code>{{username}}</code> ist nicht in den Gruppen, die von der Ressource <code>{{resource}}</code> benötigt werden.",
|
||||
"unauthorizedIpSubtitle": "Ihre IP-Adresse <code>{{ip}}</code> ist nicht berechtigt, auf die Ressource <code>{{resource}}</code> zuzugreifen.",
|
||||
"unauthorizedButton": "Erneut versuchen",
|
||||
"untrustedRedirectTitle": "Nicht vertrauenswürdige Weiterleitung",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
"untrustedRedirectSubtitle": "Sie versuchen auf eine Domain umzuleiten, die nicht mit Ihrer konfigurierten Domain übereinstimmt (<code>{{domain}}</code>). Sind Sie sicher, dass Sie fortfahren möchten?",
|
||||
"cancelTitle": "Abbrechen",
|
||||
"forgotPasswordTitle": "Passwort vergessen?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"failedToFetchProvidersTitle": "Fehler beim Laden der Authentifizierungsanbieter. Bitte überprüfen Sie Ihre Konfiguration.",
|
||||
"errorTitle": "Ein Fehler ist aufgetreten",
|
||||
"errorSubtitle": "Beim Versuch, diese Aktion auszuführen, ist ein Fehler aufgetreten. Bitte überprüfen Sie die Konsole für weitere Informationen.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
"unauthorizedResourceSubtitle": "Ο χρήστης με όνομα χρήστη <code>{{username}}</code> δεν έχει άδεια πρόσβασης στον πόρο <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "Ο χρήστης με όνομα χρήστη <code>{{username}}</code> δεν είναι εξουσιοδοτημένος να συνδεθεί.",
|
||||
"unauthorizedGroupsSubtitle": "Ο χρήστης με όνομα χρήστη <code>{{username}}</code> δεν είναι στις ομάδες που απαιτούνται από τον πόρο <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Η διεύθυνση IP σας <code>{{ip}}</code> δεν είναι εξουσιοδοτημένη να έχει πρόσβαση στον πόρο <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Προσπαθήστε ξανά",
|
||||
"untrustedRedirectTitle": "Μη έμπιστη ανακατεύθυνση",
|
||||
"untrustedRedirectSubtitle": "Προσπαθείτε να ανακατευθύνετε σε ένα domain που δεν ταιριάζει με τον ρυθμισμένο domain σας (<code>{{domain}}</code>). Είστε βέβαιοι ότι θέλετε να συνεχίσετε;",
|
||||
@@ -49,5 +50,8 @@
|
||||
"forgotPasswordTitle": "Ξεχάσατε το συνθηματικό σας;",
|
||||
"failedToFetchProvidersTitle": "Αποτυχία φόρτωσης παρόχων πιστοποίησης. Παρακαλώ ελέγξτε τις ρυθμίσεις σας.",
|
||||
"errorTitle": "Παρουσιάστηκε ένα σφάλμα",
|
||||
"errorSubtitle": "Παρουσιάστηκε σφάλμα κατά την προσπάθεια εκτέλεσης αυτής της ενέργειας. Ελέγξτε την κονσόλα για περισσότερες πληροφορίες."
|
||||
"errorSubtitle": "Παρουσιάστηκε σφάλμα κατά την προσπάθεια εκτέλεσης αυτής της ενέργειας. Ελέγξτε την κονσόλα για περισσότερες πληροφορίες.",
|
||||
"forgotPasswordMessage": "Μπορείτε να επαναφέρετε τον κωδικό πρόσβασής σας αλλάζοντας τη μεταβλητή περιβάλλοντος `USERS`.",
|
||||
"fieldRequired": "Αυτό το πεδίο είναι υποχρεωτικό",
|
||||
"invalidInput": "Μη έγκυρη καταχώρηση"
|
||||
}
|
||||
@@ -14,14 +14,17 @@
|
||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
||||
"loginOauthSuccessTitle": "Redirecting",
|
||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
||||
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
|
||||
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
|
||||
"loginOauthAutoRedirectButton": "Redirect now",
|
||||
"continueTitle": "Continue",
|
||||
"continueRedirectingTitle": "Redirecting...",
|
||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
||||
"continueRedirectManually": "Redirect me manually",
|
||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
|
||||
"continueTitle": "Continue",
|
||||
"continueSubtitle": "Click the button to continue to your app.",
|
||||
"continueUntrustedRedirectTitle": "Untrusted redirect",
|
||||
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
|
||||
"logoutFailTitle": "Failed to log out",
|
||||
"logoutFailSubtitle": "Please try again",
|
||||
"logoutSuccessTitle": "Logged out",
|
||||
@@ -42,12 +45,18 @@
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Try again",
|
||||
"untrustedRedirectTitle": "Untrusted redirect",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
"cancelTitle": "Cancel",
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input",
|
||||
"domainWarningTitle": "Invalid Domain",
|
||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||
"ignoreTitle": "Ignore",
|
||||
"goToCorrectDomainTitle": "Go to correct domain"
|
||||
}
|
||||
@@ -14,14 +14,17 @@
|
||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
||||
"loginOauthSuccessTitle": "Redirecting",
|
||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
||||
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
|
||||
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
|
||||
"loginOauthAutoRedirectButton": "Redirect now",
|
||||
"continueTitle": "Continue",
|
||||
"continueRedirectingTitle": "Redirecting...",
|
||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
||||
"continueRedirectManually": "Redirect me manually",
|
||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
|
||||
"continueTitle": "Continue",
|
||||
"continueSubtitle": "Click the button to continue to your app.",
|
||||
"continueUntrustedRedirectTitle": "Untrusted redirect",
|
||||
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
|
||||
"logoutFailTitle": "Failed to log out",
|
||||
"logoutFailSubtitle": "Please try again",
|
||||
"logoutSuccessTitle": "Logged out",
|
||||
@@ -42,12 +45,18 @@
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Try again",
|
||||
"untrustedRedirectTitle": "Untrusted redirect",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
"cancelTitle": "Cancel",
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input",
|
||||
"domainWarningTitle": "Invalid Domain",
|
||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||
"ignoreTitle": "Ignore",
|
||||
"goToCorrectDomainTitle": "Go to correct domain"
|
||||
}
|
||||
@@ -1,53 +1,57 @@
|
||||
{
|
||||
"loginTitle": "Welcome back, login with",
|
||||
"loginTitleSimple": "Welcome back, please login",
|
||||
"loginDivider": "Or",
|
||||
"loginUsername": "Username",
|
||||
"loginPassword": "Password",
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times. Please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "An error occurred",
|
||||
"loginTitle": "Bienvenido de vuelta, inicie sesión con",
|
||||
"loginTitleSimple": "Bienvenido de vuelta, por favor inicie sesión",
|
||||
"loginDivider": "O",
|
||||
"loginUsername": "Usuario",
|
||||
"loginPassword": "Contraseña",
|
||||
"loginSubmit": "Iniciar sesión",
|
||||
"loginFailTitle": "Fallo al iniciar sesión",
|
||||
"loginFailSubtitle": "Por favor revise su usuario y contraseña",
|
||||
"loginFailRateLimit": "Muchos inicios de sesión consecutivos fallidos. Por favor inténtelo más tarde",
|
||||
"loginSuccessTitle": "Sesión iniciada",
|
||||
"loginSuccessSubtitle": "¡Bienvenido de vuelta!",
|
||||
"loginOauthFailTitle": "Ocurrió un error",
|
||||
"loginOauthFailSubtitle": "Error al obtener la URL de OAuth",
|
||||
"loginOauthSuccessTitle": "Redirecting",
|
||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
||||
"continueRedirectingTitle": "Redirecting...",
|
||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
|
||||
"continueTitle": "Continue",
|
||||
"continueSubtitle": "Click the button to continue to your app.",
|
||||
"logoutFailTitle": "Failed to log out",
|
||||
"logoutFailSubtitle": "Please try again",
|
||||
"logoutSuccessTitle": "Logged out",
|
||||
"logoutSuccessSubtitle": "You have been logged out",
|
||||
"logoutTitle": "Logout",
|
||||
"logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.",
|
||||
"logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.",
|
||||
"notFoundTitle": "Page not found",
|
||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
||||
"notFoundButton": "Go home",
|
||||
"totpFailTitle": "Failed to verify code",
|
||||
"totpFailSubtitle": "Please check your code and try again",
|
||||
"totpSuccessTitle": "Verified",
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"totpSubtitle": "Please enter the code from your authenticator app.",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Try again",
|
||||
"untrustedRedirectTitle": "Untrusted redirect",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
"cancelTitle": "Cancel",
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"loginOauthSuccessTitle": "Redireccionando",
|
||||
"loginOauthSuccessSubtitle": "Redireccionando a tu proveedor de OAuth",
|
||||
"continueRedirectingTitle": "Redireccionando...",
|
||||
"continueRedirectingSubtitle": "Pronto será redirigido a la aplicación",
|
||||
"continueInvalidRedirectTitle": "Redirección inválida",
|
||||
"continueInvalidRedirectSubtitle": "La URL de redirección es inválida",
|
||||
"continueInsecureRedirectTitle": "Redirección insegura",
|
||||
"continueInsecureRedirectSubtitle": "Está intentando redirigir desde <code>https</code> a <code>http</code> lo cual no es seguro. ¿Está seguro que desea continuar?",
|
||||
"continueTitle": "Continuar",
|
||||
"continueSubtitle": "Haga clic en el botón para continuar hacia su aplicación.",
|
||||
"logoutFailTitle": "Fallo al cerrar sesión",
|
||||
"logoutFailSubtitle": "Por favor intente nuevamente",
|
||||
"logoutSuccessTitle": "Sesión cerrada",
|
||||
"logoutSuccessSubtitle": "Su sesión ha sido cerrada",
|
||||
"logoutTitle": "Cerrar sesión",
|
||||
"logoutUsernameSubtitle": "Actualmente está conectado como <code>{{username}}</code>. Haga clic en el botón de abajo para cerrar sesión.",
|
||||
"logoutOauthSubtitle": "Actualmente está conectado como <code>{{username}}</code> usando {{provider}} como su proveedor de OAuth. Haga clic en el botón de abajo para cerrar sesión.",
|
||||
"notFoundTitle": "Página no encontrada",
|
||||
"notFoundSubtitle": "La página que está buscando no existe.",
|
||||
"notFoundButton": "Volver al inicio",
|
||||
"totpFailTitle": "Error al verificar código",
|
||||
"totpFailSubtitle": "Por favor compruebe su código e inténtelo de nuevo",
|
||||
"totpSuccessTitle": "Verificado",
|
||||
"totpSuccessSubtitle": "Redirigiendo a su aplicación",
|
||||
"totpTitle": "Ingrese su código TOTP",
|
||||
"totpSubtitle": "Por favor introduzca el código de su aplicación de autenticación.",
|
||||
"unauthorizedTitle": "No autorizado",
|
||||
"unauthorizedResourceSubtitle": "El usuario con nombre de usuario <code>{{username}}</code> no está autorizado para acceder al recurso <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "El usuario con nombre de usuario <code>{{username}}</code> no está autorizado a iniciar sesión.",
|
||||
"unauthorizedGroupsSubtitle": "El usuario con nombre de usuario <code>{{username}}</code> no está en los grupos requeridos por el recurso <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Inténtelo de nuevo",
|
||||
"untrustedRedirectTitle": "Redirección no confiable",
|
||||
"untrustedRedirectSubtitle": "Está intentando redirigir a un dominio que no coincide con su dominio configurado (<code>{{domain}}</code>). ¿Está seguro que desea continuar?",
|
||||
"cancelTitle": "Cancelar",
|
||||
"forgotPasswordTitle": "¿Olvidó su contraseña?",
|
||||
"failedToFetchProvidersTitle": "Error al cargar los proveedores de autenticación. Por favor revise su configuración.",
|
||||
"errorTitle": "Ha ocurrido un error",
|
||||
"errorSubtitle": "Ocurrió un error mientras se trataba de realizar esta acción. Por favor, revise la consola para más información.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Try again",
|
||||
"untrustedRedirectTitle": "Untrusted redirect",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
@@ -49,5 +50,8 @@
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"loginTitle": "Bienvenue, connectez-vous avec",
|
||||
"loginTitleSimple": "Welcome back, please login",
|
||||
"loginDivider": "Or",
|
||||
"loginTitleSimple": "De retour parmi nous, veuillez vous connecter",
|
||||
"loginDivider": "Ou",
|
||||
"loginUsername": "Nom d'utilisateur",
|
||||
"loginPassword": "Mot de passe",
|
||||
"loginSubmit": "Se connecter",
|
||||
"loginFailTitle": "Échec de la connexion",
|
||||
"loginFailSubtitle": "Veuillez vérifier votre nom d'utilisateur et votre mot de passe",
|
||||
"loginFailRateLimit": "You failed to login too many times. Please try again later",
|
||||
"loginFailRateLimit": "Vous avez échoué trop de fois à vous connecter. Veuillez réessayer ultérieurement",
|
||||
"loginSuccessTitle": "Connecté",
|
||||
"loginSuccessSubtitle": "Bienvenue!",
|
||||
"loginOauthFailTitle": "An error occurred",
|
||||
"loginSuccessSubtitle": "Bienvenue !",
|
||||
"loginOauthFailTitle": "Une erreur s'est produite",
|
||||
"loginOauthFailSubtitle": "Impossible d'obtenir l'URL OAuth",
|
||||
"loginOauthSuccessTitle": "Redirection",
|
||||
"loginOauthSuccessSubtitle": "Redirection vers votre fournisseur OAuth",
|
||||
@@ -19,7 +19,7 @@
|
||||
"continueInvalidRedirectTitle": "Redirection invalide",
|
||||
"continueInvalidRedirectSubtitle": "L'URL de redirection est invalide",
|
||||
"continueInsecureRedirectTitle": "Redirection non sécurisée",
|
||||
"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": "Vous tentez de rediriger de <code>https</code> vers <code>http</code>, ce qui n'est pas sécurisé. Êtes-vous sûr de vouloir continuer ?",
|
||||
"continueTitle": "Continuer",
|
||||
"continueSubtitle": "Cliquez sur le bouton pour continuer vers votre application.",
|
||||
"logoutFailTitle": "Échec de la déconnexion",
|
||||
@@ -27,8 +27,8 @@
|
||||
"logoutSuccessTitle": "Déconnecté",
|
||||
"logoutSuccessSubtitle": "Vous avez été déconnecté",
|
||||
"logoutTitle": "Déconnexion",
|
||||
"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.",
|
||||
"logoutUsernameSubtitle": "Vous êtes actuellement connecté en tant que <code>{{username}}</code>. Cliquez sur le bouton ci-dessous pour vous déconnecter.",
|
||||
"logoutOauthSubtitle": "Vous êtes actuellement connecté en tant que <code>{{username}}</code> via le fournisseur OAuth {{provider}}. Cliquez sur le bouton ci-dessous pour vous déconnecter.",
|
||||
"notFoundTitle": "Page introuvable",
|
||||
"notFoundSubtitle": "La page recherchée n'existe pas.",
|
||||
"notFoundButton": "Retour à la page d'accueil",
|
||||
@@ -37,17 +37,21 @@
|
||||
"totpSuccessTitle": "Vérifié",
|
||||
"totpSuccessSubtitle": "Redirection vers votre application",
|
||||
"totpTitle": "Saisissez votre code TOTP",
|
||||
"totpSubtitle": "Please enter the code from your authenticator app.",
|
||||
"unauthorizedTitle": "Non autorisé",
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"totpSubtitle": "Veuillez saisir le code de votre application d'authentification.",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "L'utilisateur avec le nom d'utilisateur <code>{{username}}</code> n'est pas autorisé à accéder à la ressource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "L'utilisateur avec le nom d'utilisateur <code>{{username}}</code> n'est pas autorisé à se connecter.",
|
||||
"unauthorizedGroupsSubtitle": "L'utilisateur avec le nom d'utilisateur <code>{{username}}</code> n'appartient pas aux groupes requis par la ressource <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Votre adresse IP <code>{{ip}}</code> n'est pas autorisée à accéder à la ressource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Réessayer",
|
||||
"untrustedRedirectTitle": "Untrusted redirect",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
"cancelTitle": "Cancel",
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"untrustedRedirectTitle": "Redirection non fiable",
|
||||
"untrustedRedirectSubtitle": "Vous tentez de rediriger vers un domaine qui ne correspond pas à votre domaine configuré (<code>{{domain}}</code>). Êtes-vous sûr de vouloir continuer ?",
|
||||
"cancelTitle": "Annuler",
|
||||
"forgotPasswordTitle": "Mot de passe oublié ?",
|
||||
"failedToFetchProvidersTitle": "Échec du chargement des fournisseurs d'authentification. Veuillez vérifier votre configuration.",
|
||||
"errorTitle": "Une erreur est survenue",
|
||||
"errorSubtitle": "Une erreur est survenue lors de l'exécution de cette action. Veuillez consulter la console pour plus d'informations.",
|
||||
"forgotPasswordMessage": "Vous pouvez réinitialiser votre mot de passe en modifiant la variable d'environnement `USERS`.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Try again",
|
||||
"untrustedRedirectTitle": "Untrusted redirect",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
@@ -49,5 +50,8 @@
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Try again",
|
||||
"untrustedRedirectTitle": "Untrusted redirect",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
@@ -49,5 +50,8 @@
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Try again",
|
||||
"untrustedRedirectTitle": "Untrusted redirect",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
@@ -49,5 +50,8 @@
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Try again",
|
||||
"untrustedRedirectTitle": "Untrusted redirect",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
@@ -49,5 +50,8 @@
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Try again",
|
||||
"untrustedRedirectTitle": "Untrusted redirect",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
@@ -49,5 +50,8 @@
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Opnieuw proberen",
|
||||
"untrustedRedirectTitle": "Untrusted redirect",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
@@ -49,5 +50,8 @@
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Try again",
|
||||
"untrustedRedirectTitle": "Untrusted redirect",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
@@ -49,5 +50,8 @@
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"loginTitle": "Witaj ponownie, zaloguj się przez",
|
||||
"loginTitleSimple": "Witaj ponownie, zaloguj się",
|
||||
"loginDivider": "lub",
|
||||
"loginDivider": "Lub",
|
||||
"loginUsername": "Nazwa użytkownika",
|
||||
"loginPassword": "Hasło",
|
||||
"loginSubmit": "Zaloguj się",
|
||||
@@ -26,7 +26,7 @@
|
||||
"logoutFailSubtitle": "Spróbuj ponownie",
|
||||
"logoutSuccessTitle": "Wylogowano",
|
||||
"logoutSuccessSubtitle": "Zostałeś wylogowany",
|
||||
"logoutTitle": "Wylogowanie",
|
||||
"logoutTitle": "Wyloguj się",
|
||||
"logoutUsernameSubtitle": "Jesteś obecnie zalogowany jako <code>{{username}}</code>. Kliknij poniższy przycisk, aby się wylogować.",
|
||||
"logoutOauthSubtitle": "Obecnie jesteś zalogowany jako <code>{{username}}</code> przy użyciu dostawcy {{provider}} OAuth. Kliknij poniższy przycisk, aby się wylogować.",
|
||||
"notFoundTitle": "Nie znaleziono strony",
|
||||
@@ -42,6 +42,7 @@
|
||||
"unauthorizedResourceSubtitle": "Użytkownik o nazwie użytkownika <code>{{username}}</code> nie ma uprawnień dostępu do zasobu <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "Użytkownik o nazwie <code>{{username}}</code> nie jest upoważniony do zalogowania się.",
|
||||
"unauthorizedGroupsSubtitle": "Użytkownik o nazwie <code>{{username}}</code> nie należy do grup wymaganych przez zasób <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Twój adres IP <code>{{ip}}</code> nie ma autoryzacji do dostępu do zasobu <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Spróbuj ponownie",
|
||||
"untrustedRedirectTitle": "Niezaufane przekierowanie",
|
||||
"untrustedRedirectSubtitle": "Próbujesz przekierować do domeny, która nie pasuje do Twojej skonfigurowanej domeny (<code>{{domain}}</code>). Czy na pewno chcesz kontynuować?",
|
||||
@@ -49,5 +50,8 @@
|
||||
"forgotPasswordTitle": "Nie pamiętasz hasła?",
|
||||
"failedToFetchProvidersTitle": "Nie udało się załadować dostawców uwierzytelniania. Sprawdź swoją konfigurację.",
|
||||
"errorTitle": "Wystąpił błąd",
|
||||
"errorSubtitle": "Wystąpił błąd podczas próby wykonania tej czynności. Sprawdź konsolę, aby uzyskać więcej informacji."
|
||||
"errorSubtitle": "Wystąpił błąd podczas próby wykonania tej czynności. Sprawdź konsolę, aby uzyskać więcej informacji.",
|
||||
"forgotPasswordMessage": "Możesz zresetować hasło, zmieniając zmienną środowiskową `USERS`.",
|
||||
"fieldRequired": "To pole jest wymagane",
|
||||
"invalidInput": "Nieprawidłowe dane wejściowe"
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Tentar novamente",
|
||||
"untrustedRedirectTitle": "Redirecionamento não confiável",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
@@ -49,5 +50,8 @@
|
||||
"forgotPasswordTitle": "Esqueceu sua senha?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Try again",
|
||||
"untrustedRedirectTitle": "Untrusted redirect",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
@@ -49,5 +50,8 @@
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Try again",
|
||||
"untrustedRedirectTitle": "Untrusted redirect",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
@@ -49,5 +50,8 @@
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
"unauthorizedResourceSubtitle": "Пользователю <code>{{username}}</code> не разрешен доступ к <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "Пользователю <code>{{username}}</code> не разрешен вход.",
|
||||
"unauthorizedGroupsSubtitle": "Пользователь <code>{{username}}</code> не состоит в группах, которым разрешен доступ к <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Ваш IP адрес <code>{{ip}}</code> не авторизован для доступа к ресурсу <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Повторить",
|
||||
"untrustedRedirectTitle": "Ненадежное перенаправление",
|
||||
"untrustedRedirectSubtitle": "Попытка перенаправить на домен, который не соответствует вашему заданному домену (<code>{{domain}}</code>). Уверены, что хотите продолжить?",
|
||||
@@ -49,5 +50,8 @@
|
||||
"forgotPasswordTitle": "Забыли пароль?",
|
||||
"failedToFetchProvidersTitle": "Не удалось загрузить провайдеров аутентификации. Пожалуйста, проверьте конфигурацию.",
|
||||
"errorTitle": "Произошла ошибка",
|
||||
"errorSubtitle": "Произошла ошибка при попытке выполнить это действие. Проверьте консоль для дополнительной информации."
|
||||
"errorSubtitle": "Произошла ошибка при попытке выполнить это действие. Проверьте консоль для дополнительной информации.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -1,53 +1,57 @@
|
||||
{
|
||||
"loginTitle": "Welcome back, login with",
|
||||
"loginTitleSimple": "Welcome back, please login",
|
||||
"loginDivider": "Or",
|
||||
"loginUsername": "Username",
|
||||
"loginPassword": "Password",
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times. Please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "An error occurred",
|
||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
||||
"loginOauthSuccessTitle": "Redirecting",
|
||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
||||
"continueRedirectingTitle": "Redirecting...",
|
||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
|
||||
"continueTitle": "Continue",
|
||||
"continueSubtitle": "Click the button to continue to your app.",
|
||||
"logoutFailTitle": "Failed to log out",
|
||||
"logoutFailSubtitle": "Please try again",
|
||||
"logoutSuccessTitle": "Logged out",
|
||||
"logoutSuccessSubtitle": "You have been logged out",
|
||||
"logoutTitle": "Logout",
|
||||
"logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.",
|
||||
"logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.",
|
||||
"notFoundTitle": "Page not found",
|
||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
||||
"notFoundButton": "Go home",
|
||||
"totpFailTitle": "Failed to verify code",
|
||||
"totpFailSubtitle": "Please check your code and try again",
|
||||
"totpSuccessTitle": "Verified",
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"totpSubtitle": "Please enter the code from your authenticator app.",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Try again",
|
||||
"untrustedRedirectTitle": "Untrusted redirect",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
"cancelTitle": "Cancel",
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"loginTitle": "Добродошли назад, пријавите се са",
|
||||
"loginTitleSimple": "Добродошли назад, молим вас пријавите се",
|
||||
"loginDivider": "Или",
|
||||
"loginUsername": "Корисничко име",
|
||||
"loginPassword": "Лозинка",
|
||||
"loginSubmit": "Пријава",
|
||||
"loginFailTitle": "Неуспешна пријава",
|
||||
"loginFailSubtitle": "Молим вас проверите ваше корисничко име и лозинку",
|
||||
"loginFailRateLimit": "Нисте успели да се пријавите превише пута. Молим вас покушајте касније",
|
||||
"loginSuccessTitle": "Пријављени",
|
||||
"loginSuccessSubtitle": "Добродошли назад!",
|
||||
"loginOauthFailTitle": "Појавила се грешка",
|
||||
"loginOauthFailSubtitle": "Неуспело преузимање OAuth адресе",
|
||||
"loginOauthSuccessTitle": "Преусмеравање",
|
||||
"loginOauthSuccessSubtitle": "Преусмеравање на вашег OAuth провајдера",
|
||||
"continueRedirectingTitle": "Преусмеравање...",
|
||||
"continueRedirectingSubtitle": "Требали би сте ускоро да будете преусмерени на апликацију",
|
||||
"continueInvalidRedirectTitle": "Неисправно преусмеравање",
|
||||
"continueInvalidRedirectSubtitle": "Адреса за преусмеравање није исправна",
|
||||
"continueInsecureRedirectTitle": "Небезбедно преусмеравање",
|
||||
"continueInsecureRedirectSubtitle": "Покушавате да преусмерите са <code>https</code> на <code>http</code> што није безбедно. Да ли желите да наставите?",
|
||||
"continueTitle": "Настави",
|
||||
"continueSubtitle": "Кликните на дугме да би сте наставили на нашу апликацију.",
|
||||
"logoutFailTitle": "Неуспешно одјављивање",
|
||||
"logoutFailSubtitle": "Молим вас покушајте поново",
|
||||
"logoutSuccessTitle": "Одјављени",
|
||||
"logoutSuccessSubtitle": "Одјављени сте",
|
||||
"logoutTitle": "Одјава",
|
||||
"logoutUsernameSubtitle": "Тренутно сте пријављени као <code>{{username}}</code>. Кликните на дугме испод да се одјавите.",
|
||||
"logoutOauthSubtitle": "Тренутно сте пријављени као <code>{{username}}</code> користећи {{provider}} OAuth провајдера. Кликните на дугме испод да се одјавите.",
|
||||
"notFoundTitle": "Страница није пронађена",
|
||||
"notFoundSubtitle": "Страница коју тражите не постоји.",
|
||||
"notFoundButton": "На почетак",
|
||||
"totpFailTitle": "Неуспело потврђивање кода",
|
||||
"totpFailSubtitle": "Молим вас проверите ваш код и покушајте поново",
|
||||
"totpSuccessTitle": "Потврђен",
|
||||
"totpSuccessSubtitle": "Преусмеравање на вашу апликацију",
|
||||
"totpTitle": "Унесите ваш TOTP код",
|
||||
"totpSubtitle": "Молим вас унесите код из ваше апликације за аутентификацију.",
|
||||
"unauthorizedTitle": "Неауторизован",
|
||||
"unauthorizedResourceSubtitle": "Корисник са корисничким именом <code>{{username}}</code> није ауторизован да приступи ресурсу <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "Корисник са корисничким именом <code>{{username}}</code> није ауторизован за пријављивање.",
|
||||
"unauthorizedGroupsSubtitle": "Корисник са корисничким именом <code>{{username}}</code> није у групама које захтева ресурс <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Ваша IP адреса <code>{{ip}}</code> није ауторизована да приступи ресурсу <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Покушајте поново",
|
||||
"untrustedRedirectTitle": "Преусмерење без поверења",
|
||||
"untrustedRedirectSubtitle": "Покушавате да преусмерите на домен који се не поклапа са подешеним доменом (<code>{{domain}}</code>). Да ли желите да наставите?",
|
||||
"cancelTitle": "Поништи",
|
||||
"forgotPasswordTitle": "Заборавили сте лозинку?",
|
||||
"failedToFetchProvidersTitle": "Није успело учитавање провајдера аутентификације. Молим вас проверите ваша подешавања.",
|
||||
"errorTitle": "Појавила се грешка",
|
||||
"errorSubtitle": "Појавила се грешка при покушају извршавања ове радње. Молим вас проверите конзолу за додатне информације.",
|
||||
"forgotPasswordMessage": "Можете поништити вашу лозинку променом `USERS` променљиве окружења.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Try again",
|
||||
"untrustedRedirectTitle": "Untrusted redirect",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
@@ -49,5 +50,8 @@
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Try again",
|
||||
"untrustedRedirectTitle": "Untrusted redirect",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
@@ -49,5 +50,8 @@
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Try again",
|
||||
"untrustedRedirectTitle": "Untrusted redirect",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
@@ -49,5 +50,8 @@
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Try again",
|
||||
"untrustedRedirectTitle": "Untrusted redirect",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
@@ -49,5 +50,8 @@
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"loginTitle": "欢迎回来,请登录",
|
||||
"loginTitleSimple": "Welcome back, please login",
|
||||
"loginDivider": "Or",
|
||||
"loginTitle": "欢迎回来,请使用以下方式登录",
|
||||
"loginTitleSimple": "欢迎回来,请登录",
|
||||
"loginDivider": "或",
|
||||
"loginUsername": "用户名",
|
||||
"loginPassword": "密码",
|
||||
"loginSubmit": "登录",
|
||||
"loginFailTitle": "登录失败",
|
||||
"loginFailSubtitle": "请检查您的用户名和密码",
|
||||
"loginFailRateLimit": "You failed to login too many times. Please try again later",
|
||||
"loginFailRateLimit": "您登录失败次数过多。请稍后再试",
|
||||
"loginSuccessTitle": "已登录",
|
||||
"loginSuccessSubtitle": "欢迎回来!",
|
||||
"loginOauthFailTitle": "An error occurred",
|
||||
"loginOauthFailTitle": "发生错误",
|
||||
"loginOauthFailSubtitle": "获取 OAuth URL 失败",
|
||||
"loginOauthSuccessTitle": "重定向中",
|
||||
"loginOauthSuccessSubtitle": "重定向到您的 OAuth 提供商",
|
||||
@@ -19,7 +19,7 @@
|
||||
"continueInvalidRedirectTitle": "无效的重定向",
|
||||
"continueInvalidRedirectSubtitle": "重定向URL无效",
|
||||
"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>可能存在风险。您确定要继续吗?",
|
||||
"continueTitle": "继续",
|
||||
"continueSubtitle": "点击按钮以继续您的应用。",
|
||||
"logoutFailTitle": "注销失败",
|
||||
@@ -27,8 +27,8 @@
|
||||
"logoutSuccessTitle": "已登出",
|
||||
"logoutSuccessSubtitle": "您已登出",
|
||||
"logoutTitle": "登出",
|
||||
"logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.",
|
||||
"logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.",
|
||||
"logoutUsernameSubtitle": "您当前登录用户为<code>{{username}}</code>。点击下方按钮注销。",
|
||||
"logoutOauthSubtitle": "您当前以<code>{{username}}</code>登录,使用的是{{provider}} OAuth 提供商。点击下方按钮注销。",
|
||||
"notFoundTitle": "无法找到页面",
|
||||
"notFoundSubtitle": "您正在查找的页面不存在。",
|
||||
"notFoundButton": "回到主页",
|
||||
@@ -37,17 +37,21 @@
|
||||
"totpSuccessTitle": "已验证",
|
||||
"totpSuccessSubtitle": "重定向到您的应用",
|
||||
"totpTitle": "输入您的 TOTP 代码",
|
||||
"totpSubtitle": "Please enter the code from your authenticator app.",
|
||||
"totpSubtitle": "请输入您身份验证器应用中的代码。",
|
||||
"unauthorizedTitle": "未授权",
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedResourceSubtitle": "用户名为<code>{{username}}</code>的用户无权访问资源<code>{{resource}}</code>。",
|
||||
"unauthorizedLoginSubtitle": "用户名为<code>{{username}}</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>.",
|
||||
"unauthorizedButton": "重试",
|
||||
"untrustedRedirectTitle": "Untrusted redirect",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
"cancelTitle": "Cancel",
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"untrustedRedirectTitle": "不可信的重定向",
|
||||
"untrustedRedirectSubtitle": "您正在尝试重定向到一个与您已配置的域名 (<code>{{domain}}</code>) 不匹配的域名。您确定要继续吗?",
|
||||
"cancelTitle": "取消",
|
||||
"forgotPasswordTitle": "忘记密码?",
|
||||
"failedToFetchProvidersTitle": "加载身份验证提供程序失败,请检查您的配置。",
|
||||
"errorTitle": "发生了错误",
|
||||
"errorSubtitle": "执行此操作时发生错误,请检查控制台以获取更多信息。",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -1,53 +1,57 @@
|
||||
{
|
||||
"loginTitle": "Welcome back, login with",
|
||||
"loginTitleSimple": "Welcome back, please login",
|
||||
"loginDivider": "Or",
|
||||
"loginUsername": "Username",
|
||||
"loginPassword": "Password",
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times. Please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "An error occurred",
|
||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
||||
"loginOauthSuccessTitle": "Redirecting",
|
||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
||||
"continueRedirectingTitle": "Redirecting...",
|
||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
|
||||
"continueTitle": "Continue",
|
||||
"continueSubtitle": "Click the button to continue to your app.",
|
||||
"logoutFailTitle": "Failed to log out",
|
||||
"logoutFailSubtitle": "Please try again",
|
||||
"logoutSuccessTitle": "Logged out",
|
||||
"logoutSuccessSubtitle": "You have been logged out",
|
||||
"logoutTitle": "Logout",
|
||||
"logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.",
|
||||
"logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.",
|
||||
"notFoundTitle": "Page not found",
|
||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
||||
"notFoundButton": "Go home",
|
||||
"totpFailTitle": "Failed to verify code",
|
||||
"totpFailSubtitle": "Please check your code and try again",
|
||||
"totpSuccessTitle": "Verified",
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"totpSubtitle": "Please enter the code from your authenticator app.",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Try again",
|
||||
"untrustedRedirectTitle": "Untrusted redirect",
|
||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||
"cancelTitle": "Cancel",
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information."
|
||||
"loginTitle": "歡迎回來,請用以下方式登入",
|
||||
"loginTitleSimple": "歡迎回來,請登入",
|
||||
"loginDivider": "或",
|
||||
"loginUsername": "帳號",
|
||||
"loginPassword": "密碼",
|
||||
"loginSubmit": "登入",
|
||||
"loginFailTitle": "登入失敗",
|
||||
"loginFailSubtitle": "請檢查您的帳號與密碼",
|
||||
"loginFailRateLimit": "登入失敗次數過多,請稍後再試",
|
||||
"loginSuccessTitle": "登入成功",
|
||||
"loginSuccessSubtitle": "歡迎回來!",
|
||||
"loginOauthFailTitle": "發生錯誤",
|
||||
"loginOauthFailSubtitle": "無法取得 OAuth 網址",
|
||||
"loginOauthSuccessTitle": "重新導向中",
|
||||
"loginOauthSuccessSubtitle": "正在將您重新導向至 OAuth 供應商",
|
||||
"continueRedirectingTitle": "重新導向中……",
|
||||
"continueRedirectingSubtitle": "您即將被重新導向至應用程式",
|
||||
"continueInvalidRedirectTitle": "無效的重新導向",
|
||||
"continueInvalidRedirectSubtitle": "重新導向的網址無效",
|
||||
"continueInsecureRedirectTitle": "不安全的重新導向",
|
||||
"continueInsecureRedirectSubtitle": "您正嘗試從安全的 <code>https</code> 重新導向至不安全的 <code>http</code>。您確定要繼續嗎?",
|
||||
"continueTitle": "繼續",
|
||||
"continueSubtitle": "點擊按鈕以繼續前往您的應用程式。",
|
||||
"logoutFailTitle": "登出失敗",
|
||||
"logoutFailSubtitle": "請再試一次",
|
||||
"logoutSuccessTitle": "登出成功",
|
||||
"logoutSuccessSubtitle": "您已成功登出",
|
||||
"logoutTitle": "登出",
|
||||
"logoutUsernameSubtitle": "您目前以 <code>{{username}}</code> 的身分登入。點擊下方按鈕以登出。",
|
||||
"logoutOauthSubtitle": "您目前使用 {{provider}} OAuth 供應商並以 <code>{{username}}</code> 的身分登入。點擊下方按鈕以登出。",
|
||||
"notFoundTitle": "找不到頁面",
|
||||
"notFoundSubtitle": "您要尋找的頁面不存在。",
|
||||
"notFoundButton": "回到首頁",
|
||||
"totpFailTitle": "驗證失敗",
|
||||
"totpFailSubtitle": "請檢查您的驗證碼並再試一次",
|
||||
"totpSuccessTitle": "驗證成功",
|
||||
"totpSuccessSubtitle": "正在重新導向至您的應用程式",
|
||||
"totpTitle": "輸入您的 TOTP 驗證碼",
|
||||
"totpSubtitle": "請輸入您驗證器應用程式中的代碼。",
|
||||
"unauthorizedTitle": "未經授權",
|
||||
"unauthorizedResourceSubtitle": "使用者 <code>{{username}}</code> 未被授權存取資源 <code>{{resource}}</code>。",
|
||||
"unauthorizedLoginSubtitle": "使用者 <code>{{username}}</code> 未被授權登入。",
|
||||
"unauthorizedGroupsSubtitle": "使用者 <code>{{username}}</code> 不在存取資源 <code>{{resource}}</code> 所需的群組中。",
|
||||
"unauthorizedIpSubtitle": "您的 IP 位址 <code>{{ip}}</code> 未被授權存取資源 <code>{{resource}}</code>。",
|
||||
"unauthorizedButton": "再試一次",
|
||||
"untrustedRedirectTitle": "不受信任的重新導向",
|
||||
"untrustedRedirectSubtitle": "您正嘗試重新導向至的網域與您設定的網域 (<code>{{domain}}</code>) 不符。您確定要繼續嗎?",
|
||||
"cancelTitle": "取消",
|
||||
"forgotPasswordTitle": "忘記密碼?",
|
||||
"failedToFetchProvidersTitle": "載入驗證供應商失敗。請檢查您的設定。",
|
||||
"errorTitle": "發生錯誤",
|
||||
"errorSubtitle": "執行此操作時發生錯誤。請檢查主控台以獲取更多資訊。",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input"
|
||||
}
|
||||
@@ -11,60 +11,101 @@ import { useUserContext } from "@/context/user-context";
|
||||
import { isValidUrl } from "@/lib/utils";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Navigate, useLocation, useNavigate } from "react-router";
|
||||
import DOMPurify from "dompurify";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const ContinuePage = () => {
|
||||
const { cookieDomain } = useAppContext();
|
||||
const { isLoggedIn } = useUserContext();
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return <Navigate to="/login" />;
|
||||
}
|
||||
|
||||
const { domain, disableContinue } = useAppContext();
|
||||
const { search } = useLocation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const searchParams = new URLSearchParams(search);
|
||||
const redirectURI = searchParams.get("redirect_uri");
|
||||
|
||||
if (!redirectURI) {
|
||||
return <Navigate to="/logout" />;
|
||||
}
|
||||
|
||||
if (!isValidUrl(DOMPurify.sanitize(redirectURI))) {
|
||||
return <Navigate to="/logout" />;
|
||||
}
|
||||
|
||||
const handleRedirect = () => {
|
||||
setLoading(true);
|
||||
window.location.href = DOMPurify.sanitize(redirectURI);
|
||||
}
|
||||
|
||||
if (disableContinue) {
|
||||
handleRedirect();
|
||||
}
|
||||
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const url = new URL(redirectURI);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showRedirectButton, setShowRedirectButton] = useState(false);
|
||||
|
||||
if (!(url.hostname == domain) && !url.hostname.endsWith(`.${domain}`)) {
|
||||
const searchParams = new URLSearchParams(search);
|
||||
const redirectUri = searchParams.get("redirect_uri");
|
||||
|
||||
const isValidRedirectUri =
|
||||
redirectUri !== null ? isValidUrl(redirectUri) : false;
|
||||
const redirectUriObj = isValidRedirectUri
|
||||
? new URL(redirectUri as string)
|
||||
: null;
|
||||
const isTrustedRedirectUri =
|
||||
redirectUriObj !== null
|
||||
? redirectUriObj.hostname === cookieDomain ||
|
||||
redirectUriObj.hostname.endsWith(`.${cookieDomain}`)
|
||||
: false;
|
||||
const isAllowedRedirectProto =
|
||||
redirectUriObj !== null
|
||||
? redirectUriObj.protocol === "https:" ||
|
||||
redirectUriObj.protocol === "http:"
|
||||
: false;
|
||||
const isHttpsDowngrade =
|
||||
redirectUriObj !== null
|
||||
? redirectUriObj.protocol === "http:" &&
|
||||
window.location.protocol === "https:"
|
||||
: false;
|
||||
|
||||
const handleRedirect = () => {
|
||||
setLoading(true);
|
||||
window.location.assign(redirectUriObj!.toString());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isLoggedIn ||
|
||||
!isValidRedirectUri ||
|
||||
!isTrustedRedirectUri ||
|
||||
!isAllowedRedirectProto ||
|
||||
isHttpsDowngrade
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto = setTimeout(() => {
|
||||
handleRedirect();
|
||||
}, 100);
|
||||
|
||||
const reveal = setTimeout(() => {
|
||||
setLoading(false);
|
||||
setShowRedirectButton(true);
|
||||
}, 5000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(auto);
|
||||
clearTimeout(reveal);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return (
|
||||
<Card className="min-w-xs sm:min-w-sm">
|
||||
<Navigate
|
||||
to={`/login?redirect_uri=${encodeURIComponent(redirectUri || "")}`}
|
||||
replace
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isValidRedirectUri || !isAllowedRedirectProto) {
|
||||
return <Navigate to="/logout" replace />;
|
||||
}
|
||||
|
||||
if (!isTrustedRedirectUri) {
|
||||
return (
|
||||
<Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-3xl">
|
||||
{t("untrustedRedirectTitle")}
|
||||
{t("continueUntrustedRedirectTitle")}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<Trans
|
||||
i18nKey="untrustedRedirectSubtitle"
|
||||
i18nKey="continueUntrustedRedirectSubtitle"
|
||||
t={t}
|
||||
components={{
|
||||
code: <code />,
|
||||
}}
|
||||
values={{ domain }}
|
||||
values={{ cookieDomain }}
|
||||
/>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
@@ -76,7 +117,11 @@ export const ContinuePage = () => {
|
||||
>
|
||||
{t("continueTitle")}
|
||||
</Button>
|
||||
<Button onClick={() => navigate("/logout")} variant="outline" disabled={loading}>
|
||||
<Button
|
||||
onClick={() => navigate("/logout")}
|
||||
variant="outline"
|
||||
disabled={loading}
|
||||
>
|
||||
{t("cancelTitle")}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
@@ -84,9 +129,9 @@ export const ContinuePage = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (url.protocol === "http:" && window.location.protocol === "https:") {
|
||||
if (isHttpsDowngrade) {
|
||||
return (
|
||||
<Card className="min-w-xs sm:min-w-sm">
|
||||
<Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-3xl">
|
||||
{t("continueInsecureRedirectTitle")}
|
||||
@@ -102,14 +147,14 @@ export const ContinuePage = () => {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex flex-col items-stretch gap-2">
|
||||
<Button
|
||||
onClick={handleRedirect}
|
||||
loading={loading}
|
||||
variant="warning"
|
||||
>
|
||||
<Button onClick={handleRedirect} loading={loading} variant="warning">
|
||||
{t("continueTitle")}
|
||||
</Button>
|
||||
<Button onClick={() => navigate("/logout")} variant="outline" disabled={loading}>
|
||||
<Button
|
||||
onClick={() => navigate("/logout")}
|
||||
variant="outline"
|
||||
disabled={loading}
|
||||
>
|
||||
{t("cancelTitle")}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
@@ -120,17 +165,18 @@ export const ContinuePage = () => {
|
||||
return (
|
||||
<Card className="min-w-xs sm:min-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-3xl">{t("continueTitle")}</CardTitle>
|
||||
<CardDescription>{t("continueSubtitle")}</CardDescription>
|
||||
<CardTitle className="text-3xl">
|
||||
{t("continueRedirectingTitle")}
|
||||
</CardTitle>
|
||||
<CardDescription>{t("continueRedirectingSubtitle")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex flex-col items-stretch">
|
||||
<Button
|
||||
onClick={handleRedirect}
|
||||
loading={loading}
|
||||
>
|
||||
{t("continueTitle")}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
{showRedirectButton && (
|
||||
<CardFooter className="flex flex-col items-stretch">
|
||||
<Button onClick={handleRedirect}>
|
||||
{t("continueRedirectManually")}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ export const ForgotPasswordPage = () => {
|
||||
<CardHeader>
|
||||
<CardTitle className="text-3xl">{t("forgotPasswordTitle")}</CardTitle>
|
||||
<CardDescription>
|
||||
<Markdown>{forgotPasswordMessage}</Markdown>
|
||||
<Markdown>{forgotPasswordMessage !== "" ? forgotPasswordMessage : t('forgotPasswordMessage')}</Markdown>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { LoginForm } from "@/components/auth/login-form";
|
||||
import { GenericIcon } from "@/components/icons/generic";
|
||||
import { GithubIcon } from "@/components/icons/github";
|
||||
import { GoogleIcon } from "@/components/icons/google";
|
||||
import { MicrosoftIcon } from "@/components/icons/microsoft";
|
||||
import { OAuthIcon } from "@/components/icons/oauth";
|
||||
import { PocketIDIcon } from "@/components/icons/pocket-id";
|
||||
import { TailscaleIcon } from "@/components/icons/tailscale";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
} from "@/components/ui/card";
|
||||
import { OAuthButton } from "@/components/ui/oauth-button";
|
||||
import { SeperatorWithChildren } from "@/components/ui/separator";
|
||||
@@ -17,30 +22,40 @@ import { useIsMounted } from "@/lib/hooks/use-is-mounted";
|
||||
import { LoginSchema } from "@/schemas/login-schema";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Navigate, useLocation } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const iconMap: Record<string, React.ReactNode> = {
|
||||
google: <GoogleIcon />,
|
||||
github: <GithubIcon />,
|
||||
tailscale: <TailscaleIcon />,
|
||||
microsoft: <MicrosoftIcon />,
|
||||
pocketid: <PocketIDIcon />,
|
||||
};
|
||||
|
||||
export const LoginPage = () => {
|
||||
const { isLoggedIn } = useUserContext();
|
||||
|
||||
if (isLoggedIn) {
|
||||
return <Navigate to="/logout" />;
|
||||
}
|
||||
|
||||
const { configuredProviders, title, oauthAutoRedirect, genericName } = useAppContext();
|
||||
const { providers, title, oauthAutoRedirect } = useAppContext();
|
||||
const { search } = useLocation();
|
||||
const { t } = useTranslation();
|
||||
const isMounted = useIsMounted();
|
||||
const [oauthAutoRedirectHandover, setOauthAutoRedirectHandover] =
|
||||
useState(false);
|
||||
const [showRedirectButton, setShowRedirectButton] = useState(false);
|
||||
|
||||
const redirectTimer = useRef<number | null>(null);
|
||||
const redirectButtonTimer = useRef<number | null>(null);
|
||||
|
||||
const searchParams = new URLSearchParams(search);
|
||||
const redirectUri = searchParams.get("redirect_uri");
|
||||
|
||||
const oauthConfigured =
|
||||
configuredProviders.filter((provider) => provider !== "username").length >
|
||||
0;
|
||||
const userAuthConfigured = configuredProviders.includes("username");
|
||||
const oauthProviders = providers.filter(
|
||||
(provider) => provider.id !== "username",
|
||||
);
|
||||
const userAuthConfigured =
|
||||
providers.find((provider) => provider.id === "username") !== undefined;
|
||||
|
||||
const oauthMutation = useMutation({
|
||||
mutationFn: (provider: string) =>
|
||||
@@ -53,11 +68,12 @@ export const LoginPage = () => {
|
||||
description: t("loginOauthSuccessSubtitle"),
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = data.data.url;
|
||||
redirectTimer.current = window.setTimeout(() => {
|
||||
window.location.replace(data.data.url);
|
||||
}, 500);
|
||||
},
|
||||
onError: () => {
|
||||
setOauthAutoRedirectHandover(false);
|
||||
toast.error(t("loginOauthFailTitle"), {
|
||||
description: t("loginOauthFailSubtitle"),
|
||||
});
|
||||
@@ -65,7 +81,7 @@ export const LoginPage = () => {
|
||||
});
|
||||
|
||||
const loginMutation = useMutation({
|
||||
mutationFn: (values: LoginSchema) => axios.post("/api/login", values),
|
||||
mutationFn: (values: LoginSchema) => axios.post("/api/user/login", values),
|
||||
mutationKey: ["login"],
|
||||
onSuccess: (data) => {
|
||||
if (data.data.totpPending) {
|
||||
@@ -79,7 +95,7 @@ export const LoginPage = () => {
|
||||
description: t("loginSuccessSubtitle"),
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
redirectTimer.current = window.setTimeout(() => {
|
||||
window.location.replace(
|
||||
`/continue?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`,
|
||||
);
|
||||
@@ -98,61 +114,99 @@ export const LoginPage = () => {
|
||||
useEffect(() => {
|
||||
if (isMounted()) {
|
||||
if (
|
||||
oauthConfigured &&
|
||||
configuredProviders.includes(oauthAutoRedirect) &&
|
||||
oauthProviders.length !== 0 &&
|
||||
providers.find((provider) => provider.id === oauthAutoRedirect) &&
|
||||
!isLoggedIn &&
|
||||
redirectUri
|
||||
) {
|
||||
setOauthAutoRedirectHandover(true);
|
||||
oauthMutation.mutate(oauthAutoRedirect);
|
||||
redirectButtonTimer.current = window.setTimeout(() => {
|
||||
setShowRedirectButton(true);
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (redirectTimer.current) clearTimeout(redirectTimer.current);
|
||||
if (redirectButtonTimer.current)
|
||||
clearTimeout(redirectButtonTimer.current);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
if (isLoggedIn && redirectUri) {
|
||||
return (
|
||||
<Navigate
|
||||
to={`/continue?redirect_uri=${encodeURIComponent(redirectUri)}`}
|
||||
replace
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoggedIn) {
|
||||
return <Navigate to="/logout" replace />;
|
||||
}
|
||||
|
||||
if (oauthAutoRedirectHandover) {
|
||||
return (
|
||||
<Card className="min-w-xs sm:min-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-3xl">
|
||||
{t("loginOauthAutoRedirectTitle")}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t("loginOauthAutoRedirectSubtitle")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
{showRedirectButton && (
|
||||
<CardFooter className="flex flex-col items-stretch">
|
||||
<Button
|
||||
onClick={() => {
|
||||
window.location.replace(oauthMutation.data?.data.url);
|
||||
}}
|
||||
>
|
||||
{t("loginOauthAutoRedirectButton")}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Card className="min-w-xs sm:min-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-center text-3xl">{title}</CardTitle>
|
||||
{configuredProviders.length > 0 && (
|
||||
{providers.length > 0 && (
|
||||
<CardDescription className="text-center">
|
||||
{oauthConfigured ? t("loginTitle") : t("loginTitleSimple")}
|
||||
{oauthProviders.length !== 0
|
||||
? t("loginTitle")
|
||||
: t("loginTitleSimple")}
|
||||
</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{oauthConfigured && (
|
||||
{oauthProviders.length !== 0 && (
|
||||
<div className="flex flex-col gap-2 items-center justify-center">
|
||||
{configuredProviders.includes("google") && (
|
||||
{oauthProviders.map((provider) => (
|
||||
<OAuthButton
|
||||
title="Google"
|
||||
icon={<GoogleIcon />}
|
||||
key={provider.id}
|
||||
title={provider.name}
|
||||
icon={iconMap[provider.id] ?? <OAuthIcon />}
|
||||
className="w-full"
|
||||
onClick={() => oauthMutation.mutate("google")}
|
||||
loading={oauthMutation.isPending && oauthMutation.variables === "google"}
|
||||
onClick={() => oauthMutation.mutate(provider.id)}
|
||||
loading={
|
||||
oauthMutation.isPending &&
|
||||
oauthMutation.variables === provider.id
|
||||
}
|
||||
disabled={oauthMutation.isPending || loginMutation.isPending}
|
||||
/>
|
||||
)}
|
||||
{configuredProviders.includes("github") && (
|
||||
<OAuthButton
|
||||
title="Github"
|
||||
icon={<GithubIcon />}
|
||||
className="w-full"
|
||||
onClick={() => oauthMutation.mutate("github")}
|
||||
loading={oauthMutation.isPending && oauthMutation.variables === "github"}
|
||||
disabled={oauthMutation.isPending || loginMutation.isPending}
|
||||
/>
|
||||
)}
|
||||
{configuredProviders.includes("generic") && (
|
||||
<OAuthButton
|
||||
title={genericName}
|
||||
icon={<GenericIcon />}
|
||||
className="w-full"
|
||||
onClick={() => oauthMutation.mutate("generic")}
|
||||
loading={oauthMutation.isPending && oauthMutation.variables === "generic"}
|
||||
disabled={oauthMutation.isPending || loginMutation.isPending}
|
||||
/>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{userAuthConfigured && oauthConfigured && (
|
||||
{userAuthConfigured && oauthProviders.length !== 0 && (
|
||||
<SeperatorWithChildren>{t("loginDivider")}</SeperatorWithChildren>
|
||||
)}
|
||||
{userAuthConfigured && (
|
||||
@@ -161,10 +215,10 @@ export const LoginPage = () => {
|
||||
loading={loginMutation.isPending || oauthMutation.isPending}
|
||||
/>
|
||||
)}
|
||||
{configuredProviders.length == 0 && (
|
||||
<h3 className="text-center text-xl text-red-600">
|
||||
{providers.length == 0 && (
|
||||
<p className="text-center text-red-600 max-w-sm">
|
||||
{t("failedToFetchProvidersTitle")}
|
||||
</h3>
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -6,35 +6,30 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { useAppContext } from "@/context/app-context";
|
||||
import { useUserContext } from "@/context/user-context";
|
||||
import { capitalize } from "@/lib/utils";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Navigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const LogoutPage = () => {
|
||||
const { provider, username, isLoggedIn, email } = useUserContext();
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return <Navigate to="/login" />;
|
||||
}
|
||||
|
||||
const { genericName } = useAppContext();
|
||||
const { provider, username, isLoggedIn, email, oauthName } = useUserContext();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const redirectTimer = useRef<number | null>(null);
|
||||
|
||||
const logoutMutation = useMutation({
|
||||
mutationFn: () => axios.post("/api/logout"),
|
||||
mutationFn: () => axios.post("/api/user/logout"),
|
||||
mutationKey: ["logout"],
|
||||
onSuccess: () => {
|
||||
toast.success(t("logoutSuccessTitle"), {
|
||||
description: t("logoutSuccessSubtitle"),
|
||||
});
|
||||
|
||||
setTimeout(async () => {
|
||||
window.location.replace("/login");
|
||||
redirectTimer.current = window.setTimeout(() => {
|
||||
window.location.assign("/login");
|
||||
}, 500);
|
||||
},
|
||||
onError: () => {
|
||||
@@ -44,6 +39,17 @@ export const LogoutPage = () => {
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (redirectTimer.current) clearTimeout(redirectTimer.current);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="min-w-xs sm:min-w-sm">
|
||||
<CardHeader>
|
||||
@@ -58,8 +64,7 @@ export const LogoutPage = () => {
|
||||
}}
|
||||
values={{
|
||||
username: email,
|
||||
provider:
|
||||
provider === "generic" ? genericName : capitalize(provider),
|
||||
provider: oauthName,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -12,34 +12,31 @@ import { useUserContext } from "@/context/user-context";
|
||||
import { TotpSchema } from "@/schemas/totp-schema";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
import { useId } from "react";
|
||||
import { useEffect, useId, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Navigate, useLocation } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const TotpPage = () => {
|
||||
const { totpPending } = useUserContext();
|
||||
|
||||
if (!totpPending) {
|
||||
return <Navigate to="/" />;
|
||||
}
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { search } = useLocation();
|
||||
const formId = useId();
|
||||
|
||||
const redirectTimer = useRef<number | null>(null);
|
||||
|
||||
const searchParams = new URLSearchParams(search);
|
||||
const redirectUri = searchParams.get("redirect_uri");
|
||||
|
||||
const totpMutation = useMutation({
|
||||
mutationFn: (values: TotpSchema) => axios.post("/api/totp", values),
|
||||
mutationFn: (values: TotpSchema) => axios.post("/api/user/totp", values),
|
||||
mutationKey: ["totp"],
|
||||
onSuccess: () => {
|
||||
toast.success(t("totpSuccessTitle"), {
|
||||
description: t("totpSuccessSubtitle"),
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
redirectTimer.current = window.setTimeout(() => {
|
||||
window.location.replace(
|
||||
`/continue?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`,
|
||||
);
|
||||
@@ -52,6 +49,17 @@ export const TotpPage = () => {
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (redirectTimer.current) clearTimeout(redirectTimer.current);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
if (!totpPending) {
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="min-w-xs sm:min-w-sm">
|
||||
<CardHeader>
|
||||
|
||||
@@ -12,25 +12,26 @@ import { Navigate, useLocation, useNavigate } from "react-router";
|
||||
|
||||
export const UnauthorizedPage = () => {
|
||||
const { search } = useLocation();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const searchParams = new URLSearchParams(search);
|
||||
const username = searchParams.get("username");
|
||||
const resource = searchParams.get("resource");
|
||||
const groupErr = searchParams.get("groupErr");
|
||||
|
||||
if (!username) {
|
||||
return <Navigate to="/" />;
|
||||
}
|
||||
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const ip = searchParams.get("ip");
|
||||
|
||||
const handleRedirect = () => {
|
||||
setLoading(true);
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
if (!username && !ip) {
|
||||
return <Navigate to="/" />;
|
||||
}
|
||||
|
||||
let i18nKey = "unauthorizedLoginSubtitle";
|
||||
|
||||
if (resource) {
|
||||
@@ -41,6 +42,10 @@ export const UnauthorizedPage = () => {
|
||||
i18nKey = "unauthorizedGroupsSubtitle";
|
||||
}
|
||||
|
||||
if (ip) {
|
||||
i18nKey = "unauthorizedIpSubtitle";
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="min-w-xs sm:min-w-sm">
|
||||
<CardHeader>
|
||||
@@ -55,6 +60,7 @@ export const UnauthorizedPage = () => {
|
||||
values={{
|
||||
username,
|
||||
resource,
|
||||
ip,
|
||||
}}
|
||||
/>
|
||||
</CardDescription>
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const providerSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
oauth: z.boolean(),
|
||||
});
|
||||
|
||||
export const appContextSchema = z.object({
|
||||
configuredProviders: z.array(z.string()),
|
||||
disableContinue: z.boolean(),
|
||||
providers: z.array(providerSchema),
|
||||
title: z.string(),
|
||||
genericName: z.string(),
|
||||
domain: z.string(),
|
||||
appUrl: z.string(),
|
||||
cookieDomain: z.string(),
|
||||
forgotPasswordMessage: z.string(),
|
||||
oauthAutoRedirect: z.enum(["none", "github", "google", "generic"]),
|
||||
backgroundImage: z.string(),
|
||||
oauthAutoRedirect: z.string(),
|
||||
});
|
||||
|
||||
export type AppContextSchema = z.infer<typeof appContextSchema>;
|
||||
|
||||
@@ -8,6 +8,7 @@ export const userContextSchema = z.object({
|
||||
provider: z.string(),
|
||||
oauth: z.boolean(),
|
||||
totpPending: z.boolean(),
|
||||
oauthName: z.string(),
|
||||
});
|
||||
|
||||
export type UserContextSchema = z.infer<typeof userContextSchema>;
|
||||
|
||||
@@ -19,6 +19,11 @@ export default defineConfig({
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, ""),
|
||||
},
|
||||
"/resources": {
|
||||
target: "http://tinyauth-backend:3000/resources",
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/resources/, ""),
|
||||
},
|
||||
},
|
||||
allowedHosts: true,
|
||||
},
|
||||
|
||||
85
go.mod
85
go.mod
@@ -1,40 +1,64 @@
|
||||
module tinyauth
|
||||
|
||||
go 1.23.2
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.3
|
||||
|
||||
require (
|
||||
github.com/cenkalti/backoff/v5 v5.0.3
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/go-playground/validator/v10 v10.26.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-playground/validator/v10 v10.27.0
|
||||
github.com/golang-migrate/migrate/v4 v4.19.0
|
||||
github.com/google/go-querystring v1.1.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mdp/qrterminal/v3 v3.2.1
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/viper v1.20.1
|
||||
golang.org/x/crypto v0.38.0
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/traefik/paerser v0.2.2
|
||||
github.com/weppos/publicsuffix-go v0.50.0
|
||||
golang.org/x/crypto v0.42.0
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
|
||||
gorm.io/gorm v1.31.0
|
||||
gotest.tools/v3 v3.5.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
gotest.tools/v3 v3.5.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/term v0.35.0 // indirect
|
||||
modernc.org/libc v1.66.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.38.2 // indirect
|
||||
rsc.io/qr v0.2.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.4.14 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/boombuler/barcode v1.0.2 // indirect
|
||||
@@ -50,29 +74,26 @@ require (
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/docker v28.2.1+incompatible
|
||||
github.com/docker/docker v28.4.0+incompatible
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-ldap/ldap/v3 v3.4.11
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/goccy/go-json v0.10.4 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.4.0
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/magiconair/properties v1.8.10
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
@@ -86,30 +107,28 @@ require (
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.12.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
|
||||
go.opentelemetry.io/otel v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||
golang.org/x/arch v0.13.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/net v0.44.0 // indirect
|
||||
golang.org/x/oauth2 v0.31.0
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
google.golang.org/protobuf v1.36.3 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
250
go.sum
250
go.sum
@@ -1,9 +1,13 @@
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
|
||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
@@ -22,6 +26,8 @@ github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
||||
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
||||
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
|
||||
@@ -68,8 +74,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v28.2.1+incompatible h1:aTSWVTDStpHbnRu0xBcGoJEjRf5EQKt6nik6Vif8sWw=
|
||||
github.com/docker/docker v28.2.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk=
|
||||
github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
@@ -82,17 +88,25 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
||||
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/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/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
|
||||
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
@@ -101,52 +115,68 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
||||
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE=
|
||||
github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
||||
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
|
||||
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
@@ -158,6 +188,8 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
|
||||
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
@@ -183,19 +215,22 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
@@ -205,121 +240,98 @@ github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/traefik/paerser v0.2.2 h1:cpzW/ZrQrBh3mdwD/jnp6aXASiUFKOVr6ldP+keJTcQ=
|
||||
github.com/traefik/paerser v0.2.2/go.mod h1:7BBDd4FANoVgaTZG+yh26jI6CA2nds7D/4VTEdIsh24=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/weppos/publicsuffix-go v0.50.0 h1:M178k6l8cnh9T1c1cStkhytVxdk5zPd6gGZf8ySIuVo=
|
||||
github.com/weppos/publicsuffix-go v0.50.0/go.mod h1:VXhClBYMlDrUsome4pOTpe68Ui0p6iQRAbyHQD1yKoU=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
|
||||
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4=
|
||||
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
|
||||
golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
|
||||
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
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.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk=
|
||||
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
|
||||
@@ -334,8 +346,36 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
|
||||
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
|
||||
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
|
||||
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
|
||||
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
||||
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
||||
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
"tinyauth/internal/assets"
|
||||
"tinyauth/internal/handlers"
|
||||
"tinyauth/internal/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func NewAPI(config types.APIConfig, handlers *handlers.Handlers) *API {
|
||||
return &API{
|
||||
Config: config,
|
||||
Handlers: handlers,
|
||||
}
|
||||
}
|
||||
|
||||
type API struct {
|
||||
Config types.APIConfig
|
||||
Router *gin.Engine
|
||||
Handlers *handlers.Handlers
|
||||
}
|
||||
|
||||
func (api *API) Init() {
|
||||
// Disable gin logs
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
// Create router and use zerolog for logs
|
||||
log.Debug().Msg("Setting up router")
|
||||
router := gin.New()
|
||||
router.Use(zerolog())
|
||||
|
||||
// Read UI assets
|
||||
log.Debug().Msg("Setting up assets")
|
||||
dist, err := fs.Sub(assets.Assets, "dist")
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to get UI assets")
|
||||
}
|
||||
|
||||
// Create file server
|
||||
log.Debug().Msg("Setting up file server")
|
||||
fileServer := http.FileServer(http.FS(dist))
|
||||
|
||||
// UI middleware
|
||||
router.Use(func(c *gin.Context) {
|
||||
// If not an API request, serve the UI
|
||||
if !strings.HasPrefix(c.Request.URL.Path, "/api") {
|
||||
// Check if the file exists
|
||||
_, err := fs.Stat(dist, strings.TrimPrefix(c.Request.URL.Path, "/"))
|
||||
|
||||
// If the file doesn't exist, serve the index.html
|
||||
if os.IsNotExist(err) {
|
||||
c.Request.URL.Path = "/"
|
||||
}
|
||||
|
||||
// Serve the file
|
||||
fileServer.ServeHTTP(c.Writer, c.Request)
|
||||
|
||||
// Stop further processing
|
||||
c.Abort()
|
||||
}
|
||||
})
|
||||
|
||||
// Set router
|
||||
api.Router = router
|
||||
}
|
||||
|
||||
func (api *API) SetupRoutes() {
|
||||
// Proxy
|
||||
api.Router.GET("/api/auth/:proxy", api.Handlers.AuthHandler)
|
||||
|
||||
// Auth
|
||||
api.Router.POST("/api/login", api.Handlers.LoginHandler)
|
||||
api.Router.POST("/api/totp", api.Handlers.TotpHandler)
|
||||
api.Router.POST("/api/logout", api.Handlers.LogoutHandler)
|
||||
|
||||
// Context
|
||||
api.Router.GET("/api/app", api.Handlers.AppHandler)
|
||||
api.Router.GET("/api/user", api.Handlers.UserHandler)
|
||||
|
||||
// OAuth
|
||||
api.Router.GET("/api/oauth/url/:provider", api.Handlers.OauthUrlHandler)
|
||||
api.Router.GET("/api/oauth/callback/:provider", api.Handlers.OauthCallbackHandler)
|
||||
|
||||
// App
|
||||
api.Router.GET("/api/healthcheck", api.Handlers.HealthcheckHandler)
|
||||
}
|
||||
|
||||
func (api *API) Run() {
|
||||
log.Info().Str("address", api.Config.Address).Int("port", api.Config.Port).Msg("Starting server")
|
||||
|
||||
// Run server
|
||||
err := api.Router.Run(fmt.Sprintf("%s:%d", api.Config.Address, api.Config.Port))
|
||||
|
||||
// Check for errors
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to start server")
|
||||
}
|
||||
}
|
||||
|
||||
// zerolog is a middleware for gin that logs requests using zerolog
|
||||
func zerolog() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Get initial time
|
||||
tStart := time.Now()
|
||||
|
||||
// Process request
|
||||
c.Next()
|
||||
|
||||
// Get status code, address, method and path
|
||||
code := c.Writer.Status()
|
||||
address := c.Request.RemoteAddr
|
||||
method := c.Request.Method
|
||||
path := c.Request.URL.Path
|
||||
|
||||
// Get latency
|
||||
latency := time.Since(tStart).String()
|
||||
|
||||
// Log request
|
||||
switch {
|
||||
case code >= 200 && code < 300:
|
||||
log.Info().Str("method", method).Str("path", path).Str("address", address).Int("status", code).Str("latency", latency).Msg("Request")
|
||||
case code >= 300 && code < 400:
|
||||
log.Warn().Str("method", method).Str("path", path).Str("address", address).Int("status", code).Str("latency", latency).Msg("Request")
|
||||
case code >= 400:
|
||||
log.Error().Str("method", method).Str("path", path).Str("address", address).Int("status", code).Str("latency", latency).Msg("Request")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,322 +0,0 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"tinyauth/internal/api"
|
||||
"tinyauth/internal/auth"
|
||||
"tinyauth/internal/docker"
|
||||
"tinyauth/internal/handlers"
|
||||
"tinyauth/internal/hooks"
|
||||
"tinyauth/internal/providers"
|
||||
"tinyauth/internal/types"
|
||||
|
||||
"github.com/magiconair/properties/assert"
|
||||
)
|
||||
|
||||
// Simple API config for tests
|
||||
var apiConfig = types.APIConfig{
|
||||
Port: 8080,
|
||||
Address: "0.0.0.0",
|
||||
}
|
||||
|
||||
// Simple handlers config for tests
|
||||
var handlersConfig = types.HandlersConfig{
|
||||
AppURL: "http://localhost:8080",
|
||||
Domain: "localhost",
|
||||
DisableContinue: false,
|
||||
CookieSecure: false,
|
||||
Title: "Tinyauth",
|
||||
GenericName: "Generic",
|
||||
ForgotPasswordMessage: "Some message",
|
||||
CsrfCookieName: "tinyauth-csrf",
|
||||
RedirectCookieName: "tinyauth-redirect",
|
||||
BackgroundImage: "https://example.com/image.png",
|
||||
OAuthAutoRedirect: "none",
|
||||
}
|
||||
|
||||
// Simple auth config for tests
|
||||
var authConfig = types.AuthConfig{
|
||||
Users: types.Users{},
|
||||
OauthWhitelist: "",
|
||||
Secret: "super-secret-api-thing-for-tests", // It is 32 chars long
|
||||
CookieSecure: false,
|
||||
SessionExpiry: 3600,
|
||||
LoginTimeout: 0,
|
||||
LoginMaxRetries: 0,
|
||||
SessionCookieName: "tinyauth-session",
|
||||
Domain: "localhost",
|
||||
}
|
||||
|
||||
// Simple hooks config for tests
|
||||
var hooksConfig = types.HooksConfig{
|
||||
Domain: "localhost",
|
||||
}
|
||||
|
||||
// Cookie
|
||||
var cookie string
|
||||
|
||||
// User
|
||||
var user = types.User{
|
||||
Username: "user",
|
||||
Password: "$2a$10$AvGHLTYv3xiRJ0xV9xs3XeVIlkGTygI9nqIamFYB5Xu.5.0UWF7B6", // pass
|
||||
}
|
||||
|
||||
// We need all this to be able to test the API
|
||||
func getAPI(t *testing.T) *api.API {
|
||||
// Create docker service
|
||||
docker := docker.NewDocker()
|
||||
|
||||
// Initialize docker
|
||||
err := docker.Init()
|
||||
|
||||
// Check if there was an error
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize docker: %v", err)
|
||||
}
|
||||
|
||||
// Create auth service
|
||||
authConfig.Users = types.Users{
|
||||
{
|
||||
Username: user.Username,
|
||||
Password: user.Password,
|
||||
},
|
||||
}
|
||||
auth := auth.NewAuth(authConfig, docker)
|
||||
|
||||
// Create providers service
|
||||
providers := providers.NewProviders(types.OAuthConfig{})
|
||||
|
||||
// Initialize providers
|
||||
providers.Init()
|
||||
|
||||
// Create hooks service
|
||||
hooks := hooks.NewHooks(hooksConfig, auth, providers)
|
||||
|
||||
// Create handlers service
|
||||
handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker)
|
||||
|
||||
// Create API
|
||||
api := api.NewAPI(apiConfig, handlers)
|
||||
|
||||
// Setup routes
|
||||
api.Init()
|
||||
api.SetupRoutes()
|
||||
|
||||
return api
|
||||
}
|
||||
|
||||
// Test login (we will need this for the other tests)
|
||||
func TestLogin(t *testing.T) {
|
||||
t.Log("Testing login")
|
||||
|
||||
// Get API
|
||||
api := getAPI(t)
|
||||
|
||||
// Create recorder
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
// Create request
|
||||
user := types.LoginRequest{
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
}
|
||||
|
||||
json, err := json.Marshal(user)
|
||||
|
||||
// Check if there was an error
|
||||
if err != nil {
|
||||
t.Fatalf("Error marshalling json: %v", err)
|
||||
}
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequest("POST", "/api/login", strings.NewReader(string(json)))
|
||||
|
||||
// Check if there was an error
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating request: %v", err)
|
||||
}
|
||||
|
||||
// Serve the request
|
||||
api.Router.ServeHTTP(recorder, req)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, recorder.Code, http.StatusOK)
|
||||
|
||||
// Get the cookie
|
||||
cookie = recorder.Result().Cookies()[0].Value
|
||||
|
||||
// Check if the cookie is set
|
||||
if cookie == "" {
|
||||
t.Fatalf("Cookie not set")
|
||||
}
|
||||
}
|
||||
|
||||
// Test app context
|
||||
func TestAppContext(t *testing.T) {
|
||||
t.Log("Testing app context")
|
||||
|
||||
// Get API
|
||||
api := getAPI(t)
|
||||
|
||||
// Create recorder
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequest("GET", "/api/app", nil)
|
||||
|
||||
// Check if there was an error
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating request: %v", err)
|
||||
}
|
||||
|
||||
// Set the cookie
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: "tinyauth",
|
||||
Value: cookie,
|
||||
})
|
||||
|
||||
// Serve the request
|
||||
api.Router.ServeHTTP(recorder, req)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, recorder.Code, http.StatusOK)
|
||||
|
||||
// Read the body of the response
|
||||
body, err := io.ReadAll(recorder.Body)
|
||||
|
||||
// Check if there was an error
|
||||
if err != nil {
|
||||
t.Fatalf("Error getting body: %v", err)
|
||||
}
|
||||
|
||||
// Unmarshal the body into the user struct
|
||||
var app types.AppContext
|
||||
|
||||
err = json.Unmarshal(body, &app)
|
||||
|
||||
// Check if there was an error
|
||||
if err != nil {
|
||||
t.Fatalf("Error unmarshalling body: %v", err)
|
||||
}
|
||||
|
||||
// Create tests values
|
||||
expected := types.AppContext{
|
||||
Status: 200,
|
||||
Message: "OK",
|
||||
ConfiguredProviders: []string{"username"},
|
||||
DisableContinue: false,
|
||||
Title: "Tinyauth",
|
||||
GenericName: "Generic",
|
||||
ForgotPasswordMessage: "Some message",
|
||||
BackgroundImage: "https://example.com/image.png",
|
||||
OAuthAutoRedirect: "none",
|
||||
Domain: "localhost",
|
||||
}
|
||||
|
||||
// We should get the username back
|
||||
if !reflect.DeepEqual(app, expected) {
|
||||
t.Fatalf("Expected %v, got %v", expected, app)
|
||||
}
|
||||
}
|
||||
|
||||
// Test user context
|
||||
func TestUserContext(t *testing.T) {
|
||||
t.Log("Testing user context")
|
||||
|
||||
// Get API
|
||||
api := getAPI(t)
|
||||
|
||||
// Create recorder
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequest("GET", "/api/user", nil)
|
||||
|
||||
// Check if there was an error
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating request: %v", err)
|
||||
}
|
||||
|
||||
// Set the cookie
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: "tinyauth-session",
|
||||
Value: cookie,
|
||||
})
|
||||
|
||||
// Serve the request
|
||||
api.Router.ServeHTTP(recorder, req)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, recorder.Code, http.StatusOK)
|
||||
|
||||
// Read the body of the response
|
||||
body, err := io.ReadAll(recorder.Body)
|
||||
|
||||
// Check if there was an error
|
||||
if err != nil {
|
||||
t.Fatalf("Error getting body: %v", err)
|
||||
}
|
||||
|
||||
// Unmarshal the body into the user struct
|
||||
type User struct {
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
var user User
|
||||
|
||||
err = json.Unmarshal(body, &user)
|
||||
|
||||
// Check if there was an error
|
||||
if err != nil {
|
||||
t.Fatalf("Error unmarshalling body: %v", err)
|
||||
}
|
||||
|
||||
// We should get the username back
|
||||
if user.Username != "user" {
|
||||
t.Fatalf("Expected user, got %s", user.Username)
|
||||
}
|
||||
}
|
||||
|
||||
// Test logout
|
||||
func TestLogout(t *testing.T) {
|
||||
t.Log("Testing logout")
|
||||
|
||||
// Get API
|
||||
api := getAPI(t)
|
||||
|
||||
// Create recorder
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequest("POST", "/api/logout", nil)
|
||||
|
||||
// Check if there was an error
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating request: %v", err)
|
||||
}
|
||||
|
||||
// Set the cookie
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: "tinyauth",
|
||||
Value: cookie,
|
||||
})
|
||||
|
||||
// Serve the request
|
||||
api.Router.ServeHTTP(recorder, req)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, recorder.Code, http.StatusOK)
|
||||
|
||||
// Check if the cookie is different (means go sessions flushed it)
|
||||
if recorder.Result().Cookies()[0].Value == cookie {
|
||||
t.Fatalf("Cookie not flushed")
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Testing for the oauth stuff
|
||||
@@ -4,7 +4,12 @@ import (
|
||||
"embed"
|
||||
)
|
||||
|
||||
// UI assets
|
||||
// Frontend
|
||||
//
|
||||
//go:embed dist
|
||||
var Assets embed.FS
|
||||
var FrontendAssets embed.FS
|
||||
|
||||
// Migrations
|
||||
//
|
||||
//go:embed migrations/*.sql
|
||||
var Migrations embed.FS
|
||||
|
||||
1
internal/assets/migrations/000001_init_sqlite.down.sql
Normal file
1
internal/assets/migrations/000001_init_sqlite.down.sql
Normal file
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS "sessions";
|
||||
10
internal/assets/migrations/000001_init_sqlite.up.sql
Normal file
10
internal/assets/migrations/000001_init_sqlite.up.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE IF NOT EXISTS "sessions" (
|
||||
"uuid" TEXT NOT NULL PRIMARY KEY UNIQUE,
|
||||
"username" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"provider" TEXT NOT NULL,
|
||||
"totp_pending" BOOLEAN NOT NULL,
|
||||
"oauth_groups" TEXT NULL,
|
||||
"expiry" INTEGER NOT NULL
|
||||
);
|
||||
1
internal/assets/migrations/000002_oauth_name.down.sql
Normal file
1
internal/assets/migrations/000002_oauth_name.down.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "sessions" DROP COLUMN "oauth_name";
|
||||
10
internal/assets/migrations/000002_oauth_name.up.sql
Normal file
10
internal/assets/migrations/000002_oauth_name.up.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
ALTER TABLE "sessions" ADD COLUMN "oauth_name" TEXT;
|
||||
|
||||
UPDATE "sessions"
|
||||
SET "oauth_name" = CASE
|
||||
WHEN LOWER("provider") = 'github' THEN 'GitHub'
|
||||
WHEN LOWER("provider") = 'google' THEN 'Google'
|
||||
ELSE UPPER(SUBSTR("provider", 1, 1)) || SUBSTR("provider", 2)
|
||||
END
|
||||
WHERE "oauth_name" IS NULL AND "provider" IS NOT NULL;
|
||||
|
||||
@@ -1,353 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"tinyauth/internal/docker"
|
||||
"tinyauth/internal/types"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func NewAuth(config types.AuthConfig, docker *docker.Docker) *Auth {
|
||||
return &Auth{
|
||||
Config: config,
|
||||
Docker: docker,
|
||||
LoginAttempts: make(map[string]*types.LoginAttempt),
|
||||
}
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
Config types.AuthConfig
|
||||
Docker *docker.Docker
|
||||
LoginAttempts map[string]*types.LoginAttempt
|
||||
LoginMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (auth *Auth) GetSession(c *gin.Context) (*sessions.Session, error) {
|
||||
// Create cookie store
|
||||
store := sessions.NewCookieStore([]byte(auth.Config.Secret))
|
||||
|
||||
// Configure cookie store
|
||||
store.Options = &sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: auth.Config.SessionExpiry,
|
||||
Secure: auth.Config.CookieSecure,
|
||||
HttpOnly: true,
|
||||
Domain: fmt.Sprintf(".%s", auth.Config.Domain),
|
||||
}
|
||||
|
||||
// Get session
|
||||
session, err := store.Get(c.Request, auth.Config.SessionCookieName)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get session")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (auth *Auth) GetUser(username string) *types.User {
|
||||
// Loop through users and return the user if the username matches
|
||||
for _, user := range auth.Config.Users {
|
||||
if user.Username == username {
|
||||
return &user
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auth *Auth) CheckPassword(user types.User, password string) bool {
|
||||
// Compare the hashed password with the password provided
|
||||
return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil
|
||||
}
|
||||
|
||||
// IsAccountLocked checks if a username or IP is locked due to too many failed login attempts
|
||||
func (auth *Auth) IsAccountLocked(identifier string) (bool, int) {
|
||||
auth.LoginMutex.RLock()
|
||||
defer auth.LoginMutex.RUnlock()
|
||||
|
||||
// Return false if rate limiting is not configured
|
||||
if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 {
|
||||
return false, 0
|
||||
}
|
||||
|
||||
// Check if the identifier exists in the map
|
||||
attempt, exists := auth.LoginAttempts[identifier]
|
||||
if !exists {
|
||||
return false, 0
|
||||
}
|
||||
|
||||
// If account is locked, check if lock time has expired
|
||||
if attempt.LockedUntil.After(time.Now()) {
|
||||
// Calculate remaining lockout time in seconds
|
||||
remaining := int(time.Until(attempt.LockedUntil).Seconds())
|
||||
return true, remaining
|
||||
}
|
||||
|
||||
// Lock has expired
|
||||
return false, 0
|
||||
}
|
||||
|
||||
// RecordLoginAttempt records a login attempt for rate limiting
|
||||
func (auth *Auth) RecordLoginAttempt(identifier string, success bool) {
|
||||
// Skip if rate limiting is not configured
|
||||
if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
auth.LoginMutex.Lock()
|
||||
defer auth.LoginMutex.Unlock()
|
||||
|
||||
// Get current attempt record or create a new one
|
||||
attempt, exists := auth.LoginAttempts[identifier]
|
||||
if !exists {
|
||||
attempt = &types.LoginAttempt{}
|
||||
auth.LoginAttempts[identifier] = attempt
|
||||
}
|
||||
|
||||
// Update last attempt time
|
||||
attempt.LastAttempt = time.Now()
|
||||
|
||||
// If successful login, reset failed attempts
|
||||
if success {
|
||||
attempt.FailedAttempts = 0
|
||||
attempt.LockedUntil = time.Time{} // Reset lock time
|
||||
return
|
||||
}
|
||||
|
||||
// Increment failed attempts
|
||||
attempt.FailedAttempts++
|
||||
|
||||
// If max retries reached, lock the account
|
||||
if attempt.FailedAttempts >= auth.Config.LoginMaxRetries {
|
||||
attempt.LockedUntil = time.Now().Add(time.Duration(auth.Config.LoginTimeout) * time.Second)
|
||||
log.Warn().Str("identifier", identifier).Int("timeout", auth.Config.LoginTimeout).Msg("Account locked due to too many failed login attempts")
|
||||
}
|
||||
}
|
||||
|
||||
func (auth *Auth) EmailWhitelisted(emailSrc string) bool {
|
||||
return utils.CheckWhitelist(auth.Config.OauthWhitelist, emailSrc)
|
||||
}
|
||||
|
||||
func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) error {
|
||||
log.Debug().Msg("Creating session cookie")
|
||||
|
||||
// Get session
|
||||
session, err := auth.GetSession(c)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get session")
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug().Msg("Setting session cookie")
|
||||
|
||||
// Calculate expiry
|
||||
var sessionExpiry int
|
||||
|
||||
if data.TotpPending {
|
||||
sessionExpiry = 3600
|
||||
} else {
|
||||
sessionExpiry = auth.Config.SessionExpiry
|
||||
}
|
||||
|
||||
// Set data
|
||||
session.Values["username"] = data.Username
|
||||
session.Values["name"] = data.Name
|
||||
session.Values["email"] = data.Email
|
||||
session.Values["provider"] = data.Provider
|
||||
session.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix()
|
||||
session.Values["totpPending"] = data.TotpPending
|
||||
session.Values["oauthGroups"] = data.OAuthGroups
|
||||
|
||||
// Save session
|
||||
err = session.Save(c.Request, c.Writer)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to save session")
|
||||
return err
|
||||
}
|
||||
|
||||
// Return nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auth *Auth) DeleteSessionCookie(c *gin.Context) error {
|
||||
log.Debug().Msg("Deleting session cookie")
|
||||
|
||||
// Get session
|
||||
session, err := auth.GetSession(c)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get session")
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete all values in the session
|
||||
for key := range session.Values {
|
||||
delete(session.Values, key)
|
||||
}
|
||||
|
||||
// Save session
|
||||
err = session.Save(c.Request, c.Writer)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to save session")
|
||||
return err
|
||||
}
|
||||
|
||||
// Return nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) {
|
||||
log.Debug().Msg("Getting session cookie")
|
||||
|
||||
// Get session
|
||||
session, err := auth.GetSession(c)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get session")
|
||||
return types.SessionCookie{}, err
|
||||
}
|
||||
|
||||
log.Debug().Msg("Got session")
|
||||
|
||||
// Get data from session
|
||||
username, usernameOk := session.Values["username"].(string)
|
||||
email, emailOk := session.Values["email"].(string)
|
||||
name, nameOk := session.Values["name"].(string)
|
||||
provider, providerOK := session.Values["provider"].(string)
|
||||
expiry, expiryOk := session.Values["expiry"].(int64)
|
||||
totpPending, totpPendingOk := session.Values["totpPending"].(bool)
|
||||
oauthGroups, oauthGroupsOk := session.Values["oauthGroups"].(string)
|
||||
|
||||
if !usernameOk || !providerOK || !expiryOk || !totpPendingOk || !emailOk || !nameOk || !oauthGroupsOk {
|
||||
log.Warn().Msg("Session cookie is invalid")
|
||||
|
||||
// If any data is missing, delete the session cookie
|
||||
auth.DeleteSessionCookie(c)
|
||||
|
||||
// Return empty cookie
|
||||
return types.SessionCookie{}, nil
|
||||
}
|
||||
|
||||
// Check if the cookie has expired
|
||||
if time.Now().Unix() > expiry {
|
||||
log.Warn().Msg("Session cookie expired")
|
||||
|
||||
// If it has, delete it
|
||||
auth.DeleteSessionCookie(c)
|
||||
|
||||
// Return empty cookie
|
||||
return types.SessionCookie{}, nil
|
||||
}
|
||||
|
||||
log.Debug().Str("username", username).Str("provider", provider).Int64("expiry", expiry).Bool("totpPending", totpPending).Str("name", name).Str("email", email).Str("oauthGroups", oauthGroups).Msg("Parsed cookie")
|
||||
|
||||
// Return the cookie
|
||||
return types.SessionCookie{
|
||||
Username: username,
|
||||
Name: name,
|
||||
Email: email,
|
||||
Provider: provider,
|
||||
TotpPending: totpPending,
|
||||
OAuthGroups: oauthGroups,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (auth *Auth) UserAuthConfigured() bool {
|
||||
// If there are users, return true
|
||||
return len(auth.Config.Users) > 0
|
||||
}
|
||||
|
||||
func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext, labels types.TinyauthLabels) bool {
|
||||
// Check if oauth is allowed
|
||||
if context.OAuth {
|
||||
log.Debug().Msg("Checking OAuth whitelist")
|
||||
return utils.CheckWhitelist(labels.OAuthWhitelist, context.Email)
|
||||
}
|
||||
|
||||
// Check users
|
||||
log.Debug().Msg("Checking users")
|
||||
|
||||
return utils.CheckWhitelist(labels.Users, context.Username)
|
||||
}
|
||||
|
||||
func (auth *Auth) OAuthGroup(c *gin.Context, context types.UserContext, labels types.TinyauthLabels) bool {
|
||||
// Check if groups are required
|
||||
if labels.OAuthGroups == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if we are using the generic oauth provider
|
||||
if context.Provider != "generic" {
|
||||
log.Debug().Msg("Not using generic provider, skipping group check")
|
||||
return true
|
||||
}
|
||||
|
||||
// Split the groups by comma (no need to parse since they are from the API response)
|
||||
oauthGroups := strings.Split(context.OAuthGroups, ",")
|
||||
|
||||
// For every group check if it is in the required groups
|
||||
for _, group := range oauthGroups {
|
||||
if utils.CheckWhitelist(labels.OAuthGroups, group) {
|
||||
log.Debug().Str("group", group).Msg("Group is in required groups")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// No groups matched
|
||||
log.Debug().Msg("No groups matched")
|
||||
|
||||
// Return false
|
||||
return false
|
||||
}
|
||||
|
||||
func (auth *Auth) AuthEnabled(c *gin.Context, labels types.TinyauthLabels) (bool, error) {
|
||||
// Get headers
|
||||
uri := c.Request.Header.Get("X-Forwarded-Uri")
|
||||
|
||||
// Check if the allowed label is empty
|
||||
if labels.Allowed == "" {
|
||||
// Auth enabled
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Compile regex
|
||||
regex, err := regexp.Compile(labels.Allowed)
|
||||
|
||||
// If there is an error, invalid regex, auth enabled
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Invalid regex")
|
||||
return true, err
|
||||
}
|
||||
|
||||
// Check if the uri matches the regex
|
||||
if regex.MatchString(uri) {
|
||||
// Auth disabled
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Auth enabled
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (auth *Auth) GetBasicAuth(c *gin.Context) *types.User {
|
||||
// Get the Authorization header
|
||||
username, password, ok := c.Request.BasicAuth()
|
||||
|
||||
// If not ok, return an empty user
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return the user
|
||||
return &types.User{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
"tinyauth/internal/auth"
|
||||
"tinyauth/internal/docker"
|
||||
"tinyauth/internal/types"
|
||||
)
|
||||
|
||||
var config = types.AuthConfig{
|
||||
Users: types.Users{},
|
||||
OauthWhitelist: "",
|
||||
SessionExpiry: 3600,
|
||||
}
|
||||
|
||||
func TestLoginRateLimiting(t *testing.T) {
|
||||
// Initialize a new auth service with 3 max retries and 5 seconds timeout
|
||||
config.LoginMaxRetries = 3
|
||||
config.LoginTimeout = 5
|
||||
authService := auth.NewAuth(config, &docker.Docker{})
|
||||
|
||||
// Test identifier
|
||||
identifier := "test_user"
|
||||
|
||||
// Test successful login - should not lock account
|
||||
t.Log("Testing successful login")
|
||||
|
||||
authService.RecordLoginAttempt(identifier, true)
|
||||
locked, _ := authService.IsAccountLocked(identifier)
|
||||
|
||||
if locked {
|
||||
t.Fatalf("Account should not be locked after successful login")
|
||||
}
|
||||
|
||||
// Test 2 failed attempts - should not lock account yet
|
||||
t.Log("Testing 2 failed login attempts")
|
||||
|
||||
authService.RecordLoginAttempt(identifier, false)
|
||||
authService.RecordLoginAttempt(identifier, false)
|
||||
locked, _ = authService.IsAccountLocked(identifier)
|
||||
|
||||
if locked {
|
||||
t.Fatalf("Account should not be locked after only 2 failed attempts")
|
||||
}
|
||||
|
||||
// Add one more failed attempt (total 3) - should lock account with maxRetries=3
|
||||
t.Log("Testing 3 failed login attempts")
|
||||
authService.RecordLoginAttempt(identifier, false)
|
||||
locked, remainingTime := authService.IsAccountLocked(identifier)
|
||||
|
||||
if !locked {
|
||||
t.Fatalf("Account should be locked after reaching max retries")
|
||||
}
|
||||
if remainingTime <= 0 || remainingTime > 5 {
|
||||
t.Fatalf("Expected remaining time between 1-5 seconds, got %d", remainingTime)
|
||||
}
|
||||
|
||||
// Test reset after waiting for timeout - use 1 second timeout for fast testing
|
||||
t.Log("Testing unlocking after timeout")
|
||||
|
||||
// Reinitialize auth service with a shorter timeout for testing
|
||||
config.LoginTimeout = 1
|
||||
config.LoginMaxRetries = 3
|
||||
authService = auth.NewAuth(config, &docker.Docker{})
|
||||
|
||||
// Add enough failed attempts to lock the account
|
||||
for i := 0; i < 3; i++ {
|
||||
authService.RecordLoginAttempt(identifier, false)
|
||||
}
|
||||
|
||||
// Verify it's locked
|
||||
locked, _ = authService.IsAccountLocked(identifier)
|
||||
if !locked {
|
||||
t.Fatalf("Account should be locked initially")
|
||||
}
|
||||
|
||||
// Wait a bit and verify it gets unlocked after timeout
|
||||
time.Sleep(1500 * time.Millisecond) // Wait longer than the timeout
|
||||
locked, _ = authService.IsAccountLocked(identifier)
|
||||
|
||||
if locked {
|
||||
t.Fatalf("Account should be unlocked after timeout period")
|
||||
}
|
||||
|
||||
// Test disabled rate limiting
|
||||
t.Log("Testing disabled rate limiting")
|
||||
config.LoginMaxRetries = 0
|
||||
config.LoginTimeout = 0
|
||||
authService = auth.NewAuth(config, &docker.Docker{})
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
authService.RecordLoginAttempt(identifier, false)
|
||||
}
|
||||
|
||||
locked, _ = authService.IsAccountLocked(identifier)
|
||||
if locked {
|
||||
t.Fatalf("Account should not be locked when rate limiting is disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentLoginAttempts(t *testing.T) {
|
||||
// Initialize a new auth service with 2 max retries and 5 seconds timeout
|
||||
config.LoginMaxRetries = 2
|
||||
config.LoginTimeout = 5
|
||||
authService := auth.NewAuth(config, &docker.Docker{})
|
||||
|
||||
// Test multiple identifiers
|
||||
identifiers := []string{"user1", "user2", "user3"}
|
||||
|
||||
// Test that locking one identifier doesn't affect others
|
||||
t.Log("Testing multiple identifiers")
|
||||
|
||||
// Add enough failed attempts to lock first user (2 attempts with maxRetries=2)
|
||||
authService.RecordLoginAttempt(identifiers[0], false)
|
||||
authService.RecordLoginAttempt(identifiers[0], false)
|
||||
|
||||
// Check if first user is locked
|
||||
locked, _ := authService.IsAccountLocked(identifiers[0])
|
||||
if !locked {
|
||||
t.Fatalf("User1 should be locked after reaching max retries")
|
||||
}
|
||||
|
||||
// Check that other users are not affected
|
||||
for i := 1; i < len(identifiers); i++ {
|
||||
locked, _ := authService.IsAccountLocked(identifiers[i])
|
||||
if locked {
|
||||
t.Fatalf("User%d should not be locked", i+1)
|
||||
}
|
||||
}
|
||||
|
||||
// Test successful login after failed attempts (but before lock)
|
||||
t.Log("Testing successful login after failed attempts but before lock")
|
||||
|
||||
// One failed attempt for user2
|
||||
authService.RecordLoginAttempt(identifiers[1], false)
|
||||
|
||||
// Successful login should reset the counter
|
||||
authService.RecordLoginAttempt(identifiers[1], true)
|
||||
|
||||
// Now try a failed login again - should not be locked as counter was reset
|
||||
authService.RecordLoginAttempt(identifiers[1], false)
|
||||
locked, _ = authService.IsAccountLocked(identifiers[1])
|
||||
if locked {
|
||||
t.Fatalf("User2 should not be locked after successful login reset")
|
||||
}
|
||||
}
|
||||
344
internal/bootstrap/app_bootstrap.go
Normal file
344
internal/bootstrap/app_bootstrap.go
Normal file
@@ -0,0 +1,344 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/controller"
|
||||
"tinyauth/internal/middleware"
|
||||
"tinyauth/internal/service"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Controller interface {
|
||||
SetupRoutes()
|
||||
}
|
||||
|
||||
type Middleware interface {
|
||||
Middleware() gin.HandlerFunc
|
||||
Init() error
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
Init() error
|
||||
}
|
||||
|
||||
type BootstrapApp struct {
|
||||
config config.Config
|
||||
uuid string
|
||||
}
|
||||
|
||||
func NewBootstrapApp(config config.Config) *BootstrapApp {
|
||||
return &BootstrapApp{
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) Setup() error {
|
||||
// Parse users
|
||||
users, err := utils.GetUsers(app.config.Users, app.config.UsersFile)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get OAuth configs
|
||||
oauthProviders, err := utils.GetOAuthProvidersConfig(os.Environ(), os.Args, app.config.AppURL)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get cookie domain
|
||||
cookieDomain, err := utils.GetCookieDomain(app.config.AppURL)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Cookie names
|
||||
appUrl, _ := url.Parse(app.config.AppURL) // Already validated
|
||||
uuid := utils.GenerateUUID(appUrl.Hostname())
|
||||
app.uuid = uuid
|
||||
cookieId := strings.Split(uuid, "-")[0]
|
||||
sessionCookieName := fmt.Sprintf("%s-%s", config.SessionCookieName, cookieId)
|
||||
csrfCookieName := fmt.Sprintf("%s-%s", config.CSRFCookieName, cookieId)
|
||||
redirectCookieName := fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId)
|
||||
|
||||
// Create configs
|
||||
authConfig := service.AuthServiceConfig{
|
||||
Users: users,
|
||||
OauthWhitelist: app.config.OAuthWhitelist,
|
||||
SessionExpiry: app.config.SessionExpiry,
|
||||
SecureCookie: app.config.SecureCookie,
|
||||
CookieDomain: cookieDomain,
|
||||
LoginTimeout: app.config.LoginTimeout,
|
||||
LoginMaxRetries: app.config.LoginMaxRetries,
|
||||
SessionCookieName: sessionCookieName,
|
||||
}
|
||||
|
||||
// Setup services
|
||||
var ldapService *service.LdapService
|
||||
|
||||
if app.config.LdapAddress != "" {
|
||||
ldapConfig := service.LdapServiceConfig{
|
||||
Address: app.config.LdapAddress,
|
||||
BindDN: app.config.LdapBindDN,
|
||||
BindPassword: app.config.LdapBindPassword,
|
||||
BaseDN: app.config.LdapBaseDN,
|
||||
Insecure: app.config.LdapInsecure,
|
||||
SearchFilter: app.config.LdapSearchFilter,
|
||||
}
|
||||
|
||||
ldapService = service.NewLdapService(ldapConfig)
|
||||
|
||||
err := ldapService.Init()
|
||||
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to initialize LDAP service, continuing without LDAP")
|
||||
ldapService = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Bootstrap database
|
||||
databaseService := service.NewDatabaseService(service.DatabaseServiceConfig{
|
||||
DatabasePath: app.config.DatabasePath,
|
||||
})
|
||||
|
||||
log.Debug().Str("service", fmt.Sprintf("%T", databaseService)).Msg("Initializing service")
|
||||
|
||||
err = databaseService.Init()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize database service: %w", err)
|
||||
}
|
||||
|
||||
database := databaseService.GetDatabase()
|
||||
|
||||
// Create services
|
||||
dockerService := service.NewDockerService()
|
||||
authService := service.NewAuthService(authConfig, dockerService, ldapService, database)
|
||||
oauthBrokerService := service.NewOAuthBrokerService(oauthProviders)
|
||||
|
||||
// Initialize services
|
||||
services := []Service{
|
||||
dockerService,
|
||||
authService,
|
||||
oauthBrokerService,
|
||||
}
|
||||
|
||||
for _, svc := range services {
|
||||
if svc != nil {
|
||||
log.Debug().Str("service", fmt.Sprintf("%T", svc)).Msg("Initializing service")
|
||||
err := svc.Init()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Configured providers
|
||||
babysit := map[string]string{
|
||||
"google": "Google",
|
||||
"github": "GitHub",
|
||||
}
|
||||
configuredProviders := make([]controller.Provider, 0)
|
||||
|
||||
for id, provider := range oauthProviders {
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if provider.Name == "" {
|
||||
if name, ok := babysit[id]; ok {
|
||||
provider.Name = name
|
||||
} else {
|
||||
provider.Name = utils.Capitalize(id)
|
||||
}
|
||||
}
|
||||
|
||||
configuredProviders = append(configuredProviders, controller.Provider{
|
||||
Name: provider.Name,
|
||||
ID: id,
|
||||
OAuth: true,
|
||||
})
|
||||
}
|
||||
|
||||
if authService.UserAuthConfigured() || ldapService != nil {
|
||||
configuredProviders = append(configuredProviders, controller.Provider{
|
||||
Name: "Username",
|
||||
ID: "username",
|
||||
OAuth: false,
|
||||
})
|
||||
}
|
||||
|
||||
log.Debug().Interface("providers", configuredProviders).Msg("Authentication providers")
|
||||
|
||||
if len(configuredProviders) == 0 {
|
||||
return fmt.Errorf("no authentication providers configured")
|
||||
}
|
||||
|
||||
// Create engine
|
||||
engine := gin.New()
|
||||
|
||||
if len(app.config.TrustedProxies) > 0 {
|
||||
err := engine.SetTrustedProxies(strings.Split(app.config.TrustedProxies, ","))
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set trusted proxies: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if config.Version != "development" {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
// Create middlewares
|
||||
var middlewares []Middleware
|
||||
|
||||
contextMiddleware := middleware.NewContextMiddleware(middleware.ContextMiddlewareConfig{
|
||||
CookieDomain: cookieDomain,
|
||||
}, authService, oauthBrokerService)
|
||||
|
||||
uiMiddleware := middleware.NewUIMiddleware()
|
||||
zerologMiddleware := middleware.NewZerologMiddleware()
|
||||
|
||||
middlewares = append(middlewares, contextMiddleware, uiMiddleware, zerologMiddleware)
|
||||
|
||||
for _, middleware := range middlewares {
|
||||
log.Debug().Str("middleware", fmt.Sprintf("%T", middleware)).Msg("Initializing middleware")
|
||||
err := middleware.Init()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize middleware %T: %w", middleware, err)
|
||||
}
|
||||
engine.Use(middleware.Middleware())
|
||||
}
|
||||
|
||||
// Create routers
|
||||
mainRouter := engine.Group("")
|
||||
apiRouter := engine.Group("/api")
|
||||
|
||||
// Create controllers
|
||||
contextController := controller.NewContextController(controller.ContextControllerConfig{
|
||||
Providers: configuredProviders,
|
||||
Title: app.config.Title,
|
||||
AppURL: app.config.AppURL,
|
||||
CookieDomain: cookieDomain,
|
||||
ForgotPasswordMessage: app.config.ForgotPasswordMessage,
|
||||
BackgroundImage: app.config.BackgroundImage,
|
||||
OAuthAutoRedirect: app.config.OAuthAutoRedirect,
|
||||
}, apiRouter)
|
||||
|
||||
oauthController := controller.NewOAuthController(controller.OAuthControllerConfig{
|
||||
AppURL: app.config.AppURL,
|
||||
SecureCookie: app.config.SecureCookie,
|
||||
CSRFCookieName: csrfCookieName,
|
||||
RedirectCookieName: redirectCookieName,
|
||||
CookieDomain: cookieDomain,
|
||||
}, apiRouter, authService, oauthBrokerService)
|
||||
|
||||
proxyController := controller.NewProxyController(controller.ProxyControllerConfig{
|
||||
AppURL: app.config.AppURL,
|
||||
}, apiRouter, dockerService, authService)
|
||||
|
||||
userController := controller.NewUserController(controller.UserControllerConfig{
|
||||
CookieDomain: cookieDomain,
|
||||
}, apiRouter, authService)
|
||||
|
||||
resourcesController := controller.NewResourcesController(controller.ResourcesControllerConfig{
|
||||
ResourcesDir: app.config.ResourcesDir,
|
||||
}, mainRouter)
|
||||
|
||||
healthController := controller.NewHealthController(apiRouter)
|
||||
|
||||
// Setup routes
|
||||
controller := []Controller{
|
||||
contextController,
|
||||
oauthController,
|
||||
proxyController,
|
||||
userController,
|
||||
healthController,
|
||||
resourcesController,
|
||||
}
|
||||
|
||||
for _, ctrl := range controller {
|
||||
log.Debug().Msgf("Setting up %T controller", ctrl)
|
||||
ctrl.SetupRoutes()
|
||||
}
|
||||
|
||||
// If analytics are not disabled, start heartbeat
|
||||
if !app.config.DisableAnalytics {
|
||||
log.Debug().Msg("Starting heartbeat routine")
|
||||
go app.heartbeat()
|
||||
}
|
||||
|
||||
// Start server
|
||||
address := fmt.Sprintf("%s:%d", app.config.Address, app.config.Port)
|
||||
log.Info().Msgf("Starting server on %s", address)
|
||||
if err := engine.Run(address); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to start server")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) heartbeat() {
|
||||
ticker := time.NewTicker(time.Duration(12) * time.Hour)
|
||||
defer ticker.Stop()
|
||||
|
||||
type heartbeat struct {
|
||||
UUID string `json:"uuid"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
var body heartbeat
|
||||
|
||||
body.UUID = app.uuid
|
||||
body.Version = config.Version
|
||||
|
||||
bodyJson, err := json.Marshal(body)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to marshal heartbeat body")
|
||||
return
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
|
||||
heartbeatURL := config.ApiServer + "/v1/instances/heartbeat"
|
||||
|
||||
for ; true; <-ticker.C {
|
||||
log.Debug().Msg("Sending heartbeat")
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, heartbeatURL, bytes.NewReader(bodyJson))
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create heartbeat request")
|
||||
continue
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
res, err := client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to send heartbeat")
|
||||
continue
|
||||
}
|
||||
|
||||
res.Body.Close()
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
log.Debug().Str("status", res.Status).Msg("Heartbeat returned non-200 status")
|
||||
}
|
||||
}
|
||||
}
|
||||
176
internal/config/config.go
Normal file
176
internal/config/config.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package config
|
||||
|
||||
// Version information, set at build time
|
||||
|
||||
var Version = "development"
|
||||
var CommitHash = "development"
|
||||
var BuildTimestamp = "0000-00-00T00:00:00Z"
|
||||
|
||||
// Cookie name templates
|
||||
|
||||
var SessionCookieName = "tinyauth-session"
|
||||
var CSRFCookieName = "tinyauth-csrf"
|
||||
var RedirectCookieName = "tinyauth-redirect"
|
||||
|
||||
// Main app config
|
||||
|
||||
type Config struct {
|
||||
Port int `mapstructure:"port" validate:"required"`
|
||||
Address string `validate:"required,ip4_addr" mapstructure:"address"`
|
||||
AppURL string `validate:"required,url" mapstructure:"app-url"`
|
||||
Users string `mapstructure:"users"`
|
||||
UsersFile string `mapstructure:"users-file"`
|
||||
SecureCookie bool `mapstructure:"secure-cookie"`
|
||||
OAuthWhitelist string `mapstructure:"oauth-whitelist"`
|
||||
OAuthAutoRedirect string `mapstructure:"oauth-auto-redirect"`
|
||||
SessionExpiry int `mapstructure:"session-expiry"`
|
||||
LogLevel string `mapstructure:"log-level" validate:"oneof=trace debug info warn error fatal panic"`
|
||||
Title string `mapstructure:"app-title"`
|
||||
LoginTimeout int `mapstructure:"login-timeout"`
|
||||
LoginMaxRetries int `mapstructure:"login-max-retries"`
|
||||
ForgotPasswordMessage string `mapstructure:"forgot-password-message"`
|
||||
BackgroundImage string `mapstructure:"background-image" validate:"required"`
|
||||
LdapAddress string `mapstructure:"ldap-address"`
|
||||
LdapBindDN string `mapstructure:"ldap-bind-dn"`
|
||||
LdapBindPassword string `mapstructure:"ldap-bind-password"`
|
||||
LdapBaseDN string `mapstructure:"ldap-base-dn"`
|
||||
LdapInsecure bool `mapstructure:"ldap-insecure"`
|
||||
LdapSearchFilter string `mapstructure:"ldap-search-filter"`
|
||||
ResourcesDir string `mapstructure:"resources-dir"`
|
||||
DatabasePath string `mapstructure:"database-path" validate:"required"`
|
||||
TrustedProxies string `mapstructure:"trusted-proxies"`
|
||||
DisableAnalytics bool `mapstructure:"disable-analytics"`
|
||||
}
|
||||
|
||||
// OAuth/OIDC config
|
||||
|
||||
type Claims struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
PreferredUsername string `json:"preferred_username"`
|
||||
Groups any `json:"groups"`
|
||||
}
|
||||
|
||||
type OAuthServiceConfig struct {
|
||||
ClientID string `key:"client-id"`
|
||||
ClientSecret string `key:"client-secret"`
|
||||
ClientSecretFile string `key:"client-secret-file"`
|
||||
Scopes []string `key:"scopes"`
|
||||
RedirectURL string `key:"redirect-url"`
|
||||
AuthURL string `key:"auth-url"`
|
||||
TokenURL string `key:"token-url"`
|
||||
UserinfoURL string `key:"user-info-url"`
|
||||
InsecureSkipVerify bool `key:"insecure-skip-verify"`
|
||||
Name string `key:"name"`
|
||||
}
|
||||
|
||||
// User/session related stuff
|
||||
|
||||
type User struct {
|
||||
Username string
|
||||
Password string
|
||||
TotpSecret string
|
||||
}
|
||||
|
||||
type UserSearch struct {
|
||||
Username string
|
||||
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
|
||||
}
|
||||
|
||||
type UserContext struct {
|
||||
Username string
|
||||
Name string
|
||||
Email string
|
||||
IsLoggedIn bool
|
||||
OAuth bool
|
||||
Provider string
|
||||
TotpPending bool
|
||||
OAuthGroups string
|
||||
TotpEnabled bool
|
||||
OAuthName string
|
||||
}
|
||||
|
||||
// API responses and queries
|
||||
|
||||
type UnauthorizedQuery struct {
|
||||
Username string `url:"username"`
|
||||
Resource string `url:"resource"`
|
||||
GroupErr bool `url:"groupErr"`
|
||||
IP string `url:"ip"`
|
||||
}
|
||||
|
||||
type RedirectQuery struct {
|
||||
RedirectURI string `url:"redirect_uri"`
|
||||
}
|
||||
|
||||
// Labels
|
||||
|
||||
type Apps struct {
|
||||
Apps map[string]App
|
||||
}
|
||||
|
||||
type App struct {
|
||||
Config AppConfig
|
||||
Users AppUsers
|
||||
OAuth AppOAuth
|
||||
IP AppIP
|
||||
Response AppResponse
|
||||
Path AppPath
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
Domain string
|
||||
}
|
||||
|
||||
type AppUsers struct {
|
||||
Allow string
|
||||
Block string
|
||||
}
|
||||
|
||||
type AppOAuth struct {
|
||||
Whitelist string
|
||||
Groups string
|
||||
}
|
||||
|
||||
type AppIP struct {
|
||||
Allow []string
|
||||
Block []string
|
||||
Bypass []string
|
||||
}
|
||||
|
||||
type AppResponse struct {
|
||||
Headers []string
|
||||
BasicAuth AppBasicAuth
|
||||
}
|
||||
|
||||
type AppBasicAuth struct {
|
||||
Username string
|
||||
Password string
|
||||
PasswordFile string
|
||||
}
|
||||
|
||||
type AppPath struct {
|
||||
Allow string
|
||||
Block string
|
||||
}
|
||||
|
||||
// Flags
|
||||
|
||||
type Providers struct {
|
||||
Providers map[string]OAuthServiceConfig
|
||||
}
|
||||
|
||||
// API server
|
||||
|
||||
var ApiServer = "https://api.tinyauth.app"
|
||||
@@ -1,28 +0,0 @@
|
||||
package constants
|
||||
|
||||
// TinyauthLabels is a list of labels that can be used in a tinyauth protected container
|
||||
var TinyauthLabels = []string{
|
||||
"tinyauth.oauth.whitelist",
|
||||
"tinyauth.users",
|
||||
"tinyauth.allowed",
|
||||
"tinyauth.headers",
|
||||
"tinyauth.oauth.groups",
|
||||
}
|
||||
|
||||
// Claims are the OIDC supported claims (including preferd username for some reason)
|
||||
type Claims struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
PreferredUsername string `json:"preferred_username"`
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
|
||||
// Version information
|
||||
var Version = "development"
|
||||
var CommitHash = "n/a"
|
||||
var BuildTimestamp = "n/a"
|
||||
|
||||
// Cookie names
|
||||
var SessionCookieName = "tinyauth-session"
|
||||
var CsrfCookieName = "tinyauth-csrf"
|
||||
var RedirectCookieName = "tinyauth-redirect"
|
||||
113
internal/controller/context_controller.go
Normal file
113
internal/controller/context_controller.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type UserContextResponse struct {
|
||||
Status int `json:"status"`
|
||||
Message string `json:"message"`
|
||||
IsLoggedIn bool `json:"isLoggedIn"`
|
||||
Username string `json:"username"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Provider string `json:"provider"`
|
||||
OAuth bool `json:"oauth"`
|
||||
TotpPending bool `json:"totpPending"`
|
||||
OAuthName string `json:"oauthName"`
|
||||
}
|
||||
|
||||
type AppContextResponse struct {
|
||||
Status int `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Providers []Provider `json:"providers"`
|
||||
Title string `json:"title"`
|
||||
AppURL string `json:"appUrl"`
|
||||
CookieDomain string `json:"cookieDomain"`
|
||||
ForgotPasswordMessage string `json:"forgotPasswordMessage"`
|
||||
BackgroundImage string `json:"backgroundImage"`
|
||||
OAuthAutoRedirect string `json:"oauthAutoRedirect"`
|
||||
}
|
||||
|
||||
type Provider struct {
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
OAuth bool `json:"oauth"`
|
||||
}
|
||||
|
||||
type ContextControllerConfig struct {
|
||||
Providers []Provider
|
||||
Title string
|
||||
AppURL string
|
||||
CookieDomain string
|
||||
ForgotPasswordMessage string
|
||||
BackgroundImage string
|
||||
OAuthAutoRedirect string
|
||||
}
|
||||
|
||||
type ContextController struct {
|
||||
config ContextControllerConfig
|
||||
router *gin.RouterGroup
|
||||
}
|
||||
|
||||
func NewContextController(config ContextControllerConfig, router *gin.RouterGroup) *ContextController {
|
||||
return &ContextController{
|
||||
config: config,
|
||||
router: router,
|
||||
}
|
||||
}
|
||||
|
||||
func (controller *ContextController) SetupRoutes() {
|
||||
contextGroup := controller.router.Group("/context")
|
||||
contextGroup.GET("/user", controller.userContextHandler)
|
||||
contextGroup.GET("/app", controller.appContextHandler)
|
||||
}
|
||||
|
||||
func (controller *ContextController) userContextHandler(c *gin.Context) {
|
||||
context, err := utils.GetContext(c)
|
||||
|
||||
userContext := UserContextResponse{
|
||||
Status: 200,
|
||||
Message: "Success",
|
||||
IsLoggedIn: context.IsLoggedIn,
|
||||
Username: context.Username,
|
||||
Name: context.Name,
|
||||
Email: context.Email,
|
||||
Provider: context.Provider,
|
||||
OAuth: context.OAuth,
|
||||
TotpPending: context.TotpPending,
|
||||
OAuthName: context.OAuthName,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("No user context found in request")
|
||||
userContext.Status = 401
|
||||
userContext.Message = "Unauthorized"
|
||||
userContext.IsLoggedIn = false
|
||||
c.JSON(200, userContext)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, userContext)
|
||||
}
|
||||
|
||||
func (controller *ContextController) appContextHandler(c *gin.Context) {
|
||||
appUrl, _ := url.Parse(controller.config.AppURL) // no need to check error, validated on startup
|
||||
|
||||
c.JSON(200, AppContextResponse{
|
||||
Status: 200,
|
||||
Message: "Success",
|
||||
Providers: controller.config.Providers,
|
||||
Title: controller.config.Title,
|
||||
AppURL: fmt.Sprintf("%s://%s", appUrl.Scheme, appUrl.Host),
|
||||
CookieDomain: controller.config.CookieDomain,
|
||||
ForgotPasswordMessage: controller.config.ForgotPasswordMessage,
|
||||
BackgroundImage: controller.config.BackgroundImage,
|
||||
OAuthAutoRedirect: controller.config.OAuthAutoRedirect,
|
||||
})
|
||||
}
|
||||
144
internal/controller/context_controller_test.go
Normal file
144
internal/controller/context_controller_test.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/controller"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
var controllerCfg = controller.ContextControllerConfig{
|
||||
Providers: []controller.Provider{
|
||||
{
|
||||
Name: "Username",
|
||||
ID: "username",
|
||||
OAuth: false,
|
||||
},
|
||||
{
|
||||
Name: "Google",
|
||||
ID: "google",
|
||||
OAuth: true,
|
||||
},
|
||||
},
|
||||
Title: "Test App",
|
||||
AppURL: "http://localhost:8080",
|
||||
CookieDomain: "localhost",
|
||||
ForgotPasswordMessage: "Contact admin to reset your password.",
|
||||
BackgroundImage: "/assets/bg.jpg",
|
||||
OAuthAutoRedirect: "google",
|
||||
}
|
||||
|
||||
var userContext = config.UserContext{
|
||||
Username: "testuser",
|
||||
Name: "testuser",
|
||||
Email: "test@example.com",
|
||||
IsLoggedIn: true,
|
||||
OAuth: false,
|
||||
Provider: "username",
|
||||
TotpPending: false,
|
||||
OAuthGroups: "",
|
||||
TotpEnabled: false,
|
||||
}
|
||||
|
||||
func setupContextController(middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) {
|
||||
// Setup
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.Default()
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
if middlewares != nil {
|
||||
for _, m := range *middlewares {
|
||||
router.Use(m)
|
||||
}
|
||||
}
|
||||
|
||||
group := router.Group("/api")
|
||||
|
||||
ctrl := controller.NewContextController(controllerCfg, group)
|
||||
ctrl.SetupRoutes()
|
||||
|
||||
return router, recorder
|
||||
}
|
||||
|
||||
func TestAppContextHandler(t *testing.T) {
|
||||
expectedRes := controller.AppContextResponse{
|
||||
Status: 200,
|
||||
Message: "Success",
|
||||
Providers: controllerCfg.Providers,
|
||||
Title: controllerCfg.Title,
|
||||
AppURL: controllerCfg.AppURL,
|
||||
CookieDomain: controllerCfg.CookieDomain,
|
||||
ForgotPasswordMessage: controllerCfg.ForgotPasswordMessage,
|
||||
BackgroundImage: controllerCfg.BackgroundImage,
|
||||
OAuthAutoRedirect: controllerCfg.OAuthAutoRedirect,
|
||||
}
|
||||
|
||||
router, recorder := setupContextController(nil)
|
||||
req := httptest.NewRequest("GET", "/api/context/app", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
|
||||
var ctrlRes controller.AppContextResponse
|
||||
|
||||
err := json.Unmarshal(recorder.Body.Bytes(), &ctrlRes)
|
||||
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, expectedRes, ctrlRes)
|
||||
}
|
||||
|
||||
func TestUserContextHandler(t *testing.T) {
|
||||
expectedRes := controller.UserContextResponse{
|
||||
Status: 200,
|
||||
Message: "Success",
|
||||
IsLoggedIn: userContext.IsLoggedIn,
|
||||
Username: userContext.Username,
|
||||
Name: userContext.Name,
|
||||
Email: userContext.Email,
|
||||
Provider: userContext.Provider,
|
||||
OAuth: userContext.OAuth,
|
||||
TotpPending: userContext.TotpPending,
|
||||
}
|
||||
|
||||
// Test with context
|
||||
router, recorder := setupContextController(&[]gin.HandlerFunc{
|
||||
func(c *gin.Context) {
|
||||
c.Set("context", &userContext)
|
||||
c.Next()
|
||||
},
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/context/user", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
|
||||
var ctrlRes controller.UserContextResponse
|
||||
|
||||
err := json.Unmarshal(recorder.Body.Bytes(), &ctrlRes)
|
||||
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, expectedRes, ctrlRes)
|
||||
|
||||
// Test no context
|
||||
expectedRes = controller.UserContextResponse{
|
||||
Status: 401,
|
||||
Message: "Unauthorized",
|
||||
IsLoggedIn: false,
|
||||
}
|
||||
|
||||
router, recorder = setupContextController(nil)
|
||||
req = httptest.NewRequest("GET", "/api/context/user", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
|
||||
err = json.Unmarshal(recorder.Body.Bytes(), &ctrlRes)
|
||||
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, expectedRes, ctrlRes)
|
||||
}
|
||||
25
internal/controller/health_controller.go
Normal file
25
internal/controller/health_controller.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package controller
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
type HealthController struct {
|
||||
router *gin.RouterGroup
|
||||
}
|
||||
|
||||
func NewHealthController(router *gin.RouterGroup) *HealthController {
|
||||
return &HealthController{
|
||||
router: router,
|
||||
}
|
||||
}
|
||||
|
||||
func (controller *HealthController) SetupRoutes() {
|
||||
controller.router.GET("/health", controller.healthHandler)
|
||||
controller.router.HEAD("/health", controller.healthHandler)
|
||||
}
|
||||
|
||||
func (controller *HealthController) healthHandler(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"status": "ok",
|
||||
"message": "Healthy",
|
||||
})
|
||||
}
|
||||
218
internal/controller/oauth_controller.go
Normal file
218
internal/controller/oauth_controller.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/service"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/go-querystring/query"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type OAuthRequest struct {
|
||||
Provider string `uri:"provider" binding:"required"`
|
||||
}
|
||||
|
||||
type OAuthControllerConfig struct {
|
||||
CSRFCookieName string
|
||||
RedirectCookieName string
|
||||
SecureCookie bool
|
||||
AppURL string
|
||||
CookieDomain string
|
||||
}
|
||||
|
||||
type OAuthController struct {
|
||||
config OAuthControllerConfig
|
||||
router *gin.RouterGroup
|
||||
auth *service.AuthService
|
||||
broker *service.OAuthBrokerService
|
||||
}
|
||||
|
||||
func NewOAuthController(config OAuthControllerConfig, router *gin.RouterGroup, auth *service.AuthService, broker *service.OAuthBrokerService) *OAuthController {
|
||||
return &OAuthController{
|
||||
config: config,
|
||||
router: router,
|
||||
auth: auth,
|
||||
broker: broker,
|
||||
}
|
||||
}
|
||||
|
||||
func (controller *OAuthController) SetupRoutes() {
|
||||
oauthGroup := controller.router.Group("/oauth")
|
||||
oauthGroup.GET("/url/:provider", controller.oauthURLHandler)
|
||||
oauthGroup.GET("/callback/:provider", controller.oauthCallbackHandler)
|
||||
}
|
||||
|
||||
func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
||||
var req OAuthRequest
|
||||
|
||||
err := c.BindUri(&req)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to bind URI")
|
||||
c.JSON(400, gin.H{
|
||||
"status": 400,
|
||||
"message": "Bad Request",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
service, exists := controller.broker.GetService(req.Provider)
|
||||
|
||||
if !exists {
|
||||
log.Warn().Msgf("OAuth provider not found: %s", req.Provider)
|
||||
c.JSON(404, gin.H{
|
||||
"status": 404,
|
||||
"message": "Not Found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
state := service.GenerateState()
|
||||
authURL := service.GetAuthURL(state)
|
||||
c.SetCookie(controller.config.CSRFCookieName, state, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
|
||||
|
||||
redirectURI := c.Query("redirect_uri")
|
||||
|
||||
if redirectURI != "" && utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain) {
|
||||
log.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.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"message": "OK",
|
||||
"url": authURL,
|
||||
})
|
||||
}
|
||||
|
||||
func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
var req OAuthRequest
|
||||
|
||||
err := c.BindUri(&req)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to bind URI")
|
||||
c.JSON(400, gin.H{
|
||||
"status": 400,
|
||||
"message": "Bad Request",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
state := c.Query("state")
|
||||
csrfCookie, err := c.Cookie(controller.config.CSRFCookieName)
|
||||
|
||||
if err != nil || state != csrfCookie {
|
||||
log.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.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
c.SetCookie(controller.config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
|
||||
|
||||
code := c.Query("code")
|
||||
service, exists := controller.broker.GetService(req.Provider)
|
||||
|
||||
if !exists {
|
||||
log.Warn().Msgf("OAuth provider not found: %s", req.Provider)
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
err = service.VerifyCode(code)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to verify OAuth code")
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := controller.broker.GetUser(req.Provider)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get user from OAuth provider")
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
if user.Email == "" {
|
||||
log.Error().Msg("OAuth provider did not return an email")
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
if !controller.auth.IsEmailWhitelisted(user.Email) {
|
||||
queries, err := query.Values(config.UnauthorizedQuery{
|
||||
Username: user.Email,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to encode unauthorized query")
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.config.AppURL, queries.Encode()))
|
||||
return
|
||||
}
|
||||
|
||||
var name string
|
||||
|
||||
if user.Name != "" {
|
||||
log.Debug().Msg("Using name from OAuth provider")
|
||||
name = user.Name
|
||||
} else {
|
||||
log.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])
|
||||
}
|
||||
|
||||
var username string
|
||||
|
||||
if user.PreferredUsername != "" {
|
||||
log.Debug().Msg("Using preferred username from OAuth provider")
|
||||
username = user.PreferredUsername
|
||||
} else {
|
||||
log.Debug().Msg("No preferred username from OAuth provider, using pseudo username")
|
||||
username = strings.Replace(user.Email, "@", "_", -1)
|
||||
}
|
||||
|
||||
err = controller.auth.CreateSessionCookie(c, &config.SessionCookie{
|
||||
Username: username,
|
||||
Name: name,
|
||||
Email: user.Email,
|
||||
Provider: req.Provider,
|
||||
OAuthGroups: utils.CoalesceToString(user.Groups),
|
||||
OAuthName: service.GetName(),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create session cookie")
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
redirectURI, err := c.Cookie(controller.config.RedirectCookieName)
|
||||
|
||||
if err != nil || !utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain) {
|
||||
log.Debug().Msg("No redirect URI cookie found, redirecting to app root")
|
||||
c.Redirect(http.StatusTemporaryRedirect, controller.config.AppURL)
|
||||
return
|
||||
}
|
||||
|
||||
queries, err := query.Values(config.RedirectQuery{
|
||||
RedirectURI: redirectURI,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to encode redirect URI query")
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
c.SetCookie(controller.config.RedirectCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/continue?%s", controller.config.AppURL, queries.Encode()))
|
||||
}
|
||||
291
internal/controller/proxy_controller.go
Normal file
291
internal/controller/proxy_controller.go
Normal file
@@ -0,0 +1,291 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/service"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/go-querystring/query"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Proxy struct {
|
||||
Proxy string `uri:"proxy" binding:"required"`
|
||||
}
|
||||
|
||||
type ProxyControllerConfig struct {
|
||||
AppURL string
|
||||
}
|
||||
|
||||
type ProxyController struct {
|
||||
config ProxyControllerConfig
|
||||
router *gin.RouterGroup
|
||||
docker *service.DockerService
|
||||
auth *service.AuthService
|
||||
}
|
||||
|
||||
func NewProxyController(config ProxyControllerConfig, router *gin.RouterGroup, docker *service.DockerService, auth *service.AuthService) *ProxyController {
|
||||
return &ProxyController{
|
||||
config: config,
|
||||
router: router,
|
||||
docker: docker,
|
||||
auth: auth,
|
||||
}
|
||||
}
|
||||
|
||||
func (controller *ProxyController) SetupRoutes() {
|
||||
proxyGroup := controller.router.Group("/auth")
|
||||
proxyGroup.GET("/:proxy", controller.proxyHandler)
|
||||
}
|
||||
|
||||
func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
var req Proxy
|
||||
|
||||
err := c.BindUri(&req)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to bind URI")
|
||||
c.JSON(400, gin.H{
|
||||
"status": 400,
|
||||
"message": "Bad Request",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Proxy != "nginx" && req.Proxy != "traefik" && req.Proxy != "caddy" {
|
||||
log.Warn().Str("proxy", req.Proxy).Msg("Invalid proxy")
|
||||
c.JSON(400, gin.H{
|
||||
"status": 400,
|
||||
"message": "Bad Request",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
isBrowser := strings.Contains(c.Request.Header.Get("Accept"), "text/html")
|
||||
|
||||
if isBrowser {
|
||||
log.Debug().Msg("Request identified as (most likely) coming from a browser")
|
||||
} else {
|
||||
log.Debug().Msg("Request identified as (most likely) coming from a non-browser client")
|
||||
}
|
||||
|
||||
uri := c.Request.Header.Get("X-Forwarded-Uri")
|
||||
proto := c.Request.Header.Get("X-Forwarded-Proto")
|
||||
host := c.Request.Header.Get("X-Forwarded-Host")
|
||||
|
||||
labels, err := controller.docker.GetLabels(host)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get labels from Docker")
|
||||
controller.handleError(c, req, isBrowser)
|
||||
return
|
||||
}
|
||||
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
if controller.auth.IsBypassedIP(labels.IP, clientIP) {
|
||||
controller.setHeaders(c, labels)
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"message": "Authenticated",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
authEnabled, err := controller.auth.IsAuthEnabled(uri, labels.Path)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to check if auth is enabled for resource")
|
||||
controller.handleError(c, req, isBrowser)
|
||||
return
|
||||
}
|
||||
|
||||
if !authEnabled {
|
||||
log.Debug().Msg("Authentication disabled for resource, allowing access")
|
||||
controller.setHeaders(c, labels)
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"message": "Authenticated",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !controller.auth.CheckIP(labels.IP, clientIP) {
|
||||
if req.Proxy == "nginx" || !isBrowser {
|
||||
c.JSON(401, gin.H{
|
||||
"status": 401,
|
||||
"message": "Unauthorized",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
queries, err := query.Values(config.UnauthorizedQuery{
|
||||
Resource: strings.Split(host, ".")[0],
|
||||
IP: clientIP,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to encode unauthorized query")
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.config.AppURL, queries.Encode()))
|
||||
return
|
||||
}
|
||||
|
||||
var userContext config.UserContext
|
||||
|
||||
context, err := utils.GetContext(c)
|
||||
|
||||
if err != nil {
|
||||
log.Debug().Msg("No user context found in request, treating as not logged in")
|
||||
userContext = config.UserContext{
|
||||
IsLoggedIn: false,
|
||||
}
|
||||
} else {
|
||||
userContext = context
|
||||
}
|
||||
|
||||
if userContext.Provider == "basic" && userContext.TotpEnabled {
|
||||
log.Debug().Msg("User has TOTP enabled, denying basic auth access")
|
||||
userContext.IsLoggedIn = false
|
||||
}
|
||||
|
||||
if userContext.IsLoggedIn {
|
||||
appAllowed := controller.auth.IsResourceAllowed(c, userContext, labels)
|
||||
|
||||
if !appAllowed {
|
||||
log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource")
|
||||
|
||||
if req.Proxy == "nginx" || !isBrowser {
|
||||
c.JSON(403, gin.H{
|
||||
"status": 403,
|
||||
"message": "Forbidden",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
queries, err := query.Values(config.UnauthorizedQuery{
|
||||
Resource: strings.Split(host, ".")[0],
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to encode unauthorized query")
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
if userContext.OAuth {
|
||||
queries.Set("username", userContext.Email)
|
||||
} else {
|
||||
queries.Set("username", userContext.Username)
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.config.AppURL, queries.Encode()))
|
||||
return
|
||||
}
|
||||
|
||||
if userContext.OAuth {
|
||||
groupOK := controller.auth.IsInOAuthGroup(c, userContext, labels.OAuth.Groups)
|
||||
|
||||
if !groupOK {
|
||||
log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements")
|
||||
|
||||
if req.Proxy == "nginx" || !isBrowser {
|
||||
c.JSON(403, gin.H{
|
||||
"status": 403,
|
||||
"message": "Forbidden",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
queries, err := query.Values(config.UnauthorizedQuery{
|
||||
Resource: strings.Split(host, ".")[0],
|
||||
GroupErr: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to encode unauthorized query")
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
if userContext.OAuth {
|
||||
queries.Set("username", userContext.Email)
|
||||
} else {
|
||||
queries.Set("username", userContext.Username)
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.config.AppURL, queries.Encode()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Header("Remote-User", utils.SanitizeHeader(userContext.Username))
|
||||
c.Header("Remote-Name", utils.SanitizeHeader(userContext.Name))
|
||||
c.Header("Remote-Email", utils.SanitizeHeader(userContext.Email))
|
||||
c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups))
|
||||
|
||||
controller.setHeaders(c, labels)
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"message": "Authenticated",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Proxy == "nginx" || !isBrowser {
|
||||
c.JSON(401, gin.H{
|
||||
"status": 401,
|
||||
"message": "Unauthorized",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
queries, err := query.Values(config.RedirectQuery{
|
||||
RedirectURI: fmt.Sprintf("%s://%s%s", proto, host, uri),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to encode redirect URI query")
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/login?%s", controller.config.AppURL, queries.Encode()))
|
||||
}
|
||||
|
||||
func (controller *ProxyController) setHeaders(c *gin.Context, labels config.App) {
|
||||
c.Header("Authorization", c.Request.Header.Get("Authorization"))
|
||||
|
||||
headers := utils.ParseHeaders(labels.Response.Headers)
|
||||
|
||||
for key, value := range headers {
|
||||
log.Debug().Str("header", key).Msg("Setting header")
|
||||
c.Header(key, value)
|
||||
}
|
||||
|
||||
basicPassword := utils.GetSecret(labels.Response.BasicAuth.Password, labels.Response.BasicAuth.PasswordFile)
|
||||
|
||||
if labels.Response.BasicAuth.Username != "" && basicPassword != "" {
|
||||
log.Debug().Str("username", labels.Response.BasicAuth.Username).Msg("Setting basic auth header")
|
||||
c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(labels.Response.BasicAuth.Username, basicPassword)))
|
||||
}
|
||||
}
|
||||
|
||||
func (controller *ProxyController) handleError(c *gin.Context, req Proxy, isBrowser bool) {
|
||||
if req.Proxy == "nginx" || !isBrowser {
|
||||
c.JSON(500, gin.H{
|
||||
"status": 500,
|
||||
"message": "Internal Server Error",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
}
|
||||
164
internal/controller/proxy_controller_test.go
Normal file
164
internal/controller/proxy_controller_test.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/controller"
|
||||
"tinyauth/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder, *service.AuthService) {
|
||||
// Setup
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.Default()
|
||||
|
||||
if middlewares != nil {
|
||||
for _, m := range *middlewares {
|
||||
router.Use(m)
|
||||
}
|
||||
}
|
||||
|
||||
group := router.Group("/api")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
// Database
|
||||
databaseService := service.NewDatabaseService(service.DatabaseServiceConfig{
|
||||
DatabasePath: "/tmp/tinyauth_test.db",
|
||||
})
|
||||
|
||||
assert.NilError(t, databaseService.Init())
|
||||
|
||||
database := databaseService.GetDatabase()
|
||||
|
||||
// Docker
|
||||
dockerService := service.NewDockerService()
|
||||
|
||||
assert.NilError(t, dockerService.Init())
|
||||
|
||||
// Auth service
|
||||
authService := service.NewAuthService(service.AuthServiceConfig{
|
||||
Users: []config.User{
|
||||
{
|
||||
Username: "testuser",
|
||||
Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test
|
||||
},
|
||||
},
|
||||
OauthWhitelist: "",
|
||||
SessionExpiry: 3600,
|
||||
SecureCookie: false,
|
||||
CookieDomain: "localhost",
|
||||
LoginTimeout: 300,
|
||||
LoginMaxRetries: 3,
|
||||
SessionCookieName: "tinyauth-session",
|
||||
}, dockerService, nil, database)
|
||||
|
||||
// Controller
|
||||
ctrl := controller.NewProxyController(controller.ProxyControllerConfig{
|
||||
AppURL: "http://localhost:8080",
|
||||
}, group, dockerService, authService)
|
||||
ctrl.SetupRoutes()
|
||||
|
||||
return router, recorder, authService
|
||||
}
|
||||
|
||||
func TestProxyHandler(t *testing.T) {
|
||||
// Setup
|
||||
router, recorder, authService := setupProxyController(t, nil)
|
||||
|
||||
// Test invalid proxy
|
||||
req := httptest.NewRequest("GET", "/api/auth/invalidproxy", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 400, recorder.Code)
|
||||
|
||||
// Test logged out user (traefik/caddy)
|
||||
recorder = httptest.NewRecorder()
|
||||
req = httptest.NewRequest("GET", "/api/auth/traefik", 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)
|
||||
recorder = httptest.NewRecorder()
|
||||
req = httptest.NewRequest("GET", "/api/auth/nginx", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
|
||||
// Test logged in user
|
||||
c := gin.CreateTestContextOnly(recorder, router)
|
||||
|
||||
err := authService.CreateSessionCookie(c, &config.SessionCookie{
|
||||
Username: "testuser",
|
||||
Name: "testuser",
|
||||
Email: "testuser@example.com",
|
||||
Provider: "username",
|
||||
TotpPending: false,
|
||||
OAuthGroups: "",
|
||||
})
|
||||
|
||||
assert.NilError(t, err)
|
||||
|
||||
cookie := c.Writer.Header().Get("Set-Cookie")
|
||||
|
||||
router, recorder, _ = setupProxyController(t, &[]gin.HandlerFunc{
|
||||
func(c *gin.Context) {
|
||||
c.Set("context", &config.UserContext{
|
||||
Username: "testuser",
|
||||
Name: "testuser",
|
||||
Email: "testuser@example.com",
|
||||
IsLoggedIn: true,
|
||||
OAuth: false,
|
||||
Provider: "username",
|
||||
TotpPending: false,
|
||||
OAuthGroups: "",
|
||||
TotpEnabled: false,
|
||||
})
|
||||
c.Next()
|
||||
},
|
||||
})
|
||||
|
||||
req = httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||
req.Header.Set("Cookie", cookie)
|
||||
req.Header.Set("Accept", "text/html")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
|
||||
assert.Equal(t, "testuser", recorder.Header().Get("Remote-User"))
|
||||
assert.Equal(t, "testuser", recorder.Header().Get("Remote-Name"))
|
||||
assert.Equal(t, "testuser@example.com", recorder.Header().Get("Remote-Email"))
|
||||
|
||||
// Ensure basic auth is disabled for TOTP enabled users
|
||||
router, recorder, _ = setupProxyController(t, &[]gin.HandlerFunc{
|
||||
func(c *gin.Context) {
|
||||
c.Set("context", &config.UserContext{
|
||||
Username: "testuser",
|
||||
Name: "testuser",
|
||||
Email: "testuser@example.com",
|
||||
IsLoggedIn: true,
|
||||
OAuth: false,
|
||||
Provider: "basic",
|
||||
TotpPending: false,
|
||||
OAuthGroups: "",
|
||||
TotpEnabled: true,
|
||||
})
|
||||
c.Next()
|
||||
},
|
||||
})
|
||||
|
||||
req = httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||
req.SetBasicAuth("testuser", "test")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
}
|
||||
42
internal/controller/resources_controller.go
Normal file
42
internal/controller/resources_controller.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ResourcesControllerConfig struct {
|
||||
ResourcesDir string
|
||||
}
|
||||
|
||||
type ResourcesController struct {
|
||||
config ResourcesControllerConfig
|
||||
router *gin.RouterGroup
|
||||
fileServer http.Handler
|
||||
}
|
||||
|
||||
func NewResourcesController(config ResourcesControllerConfig, router *gin.RouterGroup) *ResourcesController {
|
||||
fileServer := http.StripPrefix("/resources", http.FileServer(http.Dir(config.ResourcesDir)))
|
||||
|
||||
return &ResourcesController{
|
||||
config: config,
|
||||
router: router,
|
||||
fileServer: fileServer,
|
||||
}
|
||||
}
|
||||
|
||||
func (controller *ResourcesController) SetupRoutes() {
|
||||
controller.router.GET("/resources/*resource", controller.resourcesHandler)
|
||||
}
|
||||
|
||||
func (controller *ResourcesController) resourcesHandler(c *gin.Context) {
|
||||
if controller.config.ResourcesDir == "" {
|
||||
c.JSON(404, gin.H{
|
||||
"status": 404,
|
||||
"message": "Resources not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
controller.fileServer.ServeHTTP(c.Writer, c.Request)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user